From 9a893d6f1329cffd949122cec040fc832c055ad7 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Mon, 9 May 2022 18:53:26 +0200 Subject: [PATCH 001/811] Configure `merge=union` for `changelog.txt` to reduce merge conflicts --- .gitattributes | 1 + 1 file changed, 1 insertion(+) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000000..8b077fbd74 --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +changelog.txt merge=union From 8029b0ca21e43064f51e9f4a4696735512487e84 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 23 Aug 2024 02:26:35 -0700 Subject: [PATCH 002/811] set or clear spouse relationships also add confirmation dialogs for disruptive actions, such as choosing a partner from a different race, which clears the selected unit --- changelog.txt | 2 +- docs/gui/pregnancy.rst | 20 +- gui/pregnancy.lua | 583 +++++++++++++++++++++++++++-------------- 3 files changed, 402 insertions(+), 203 deletions(-) diff --git a/changelog.txt b/changelog.txt index 2c84dd719c..c469dbed9d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,7 +29,7 @@ Template for new versions: ## New Tools - `embark-anyone`: allows you to embark as any civilisation, including dead, and non-dwarven ones - `idle-crafting`: Allow dwarfs to automatically satisfy their need to craft objects. -- `gui/pregnancy`: view and generate pregnancies with specified parents +- `gui/pregnancy`: view and generate pregnancies or arrange marriages with specified partners ## New Features - `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them diff --git a/docs/gui/pregnancy.rst b/docs/gui/pregnancy.rst index 9ed48161d4..334832a521 100644 --- a/docs/gui/pregnancy.rst +++ b/docs/gui/pregnancy.rst @@ -6,7 +6,7 @@ gui/pregnancy :tags: adventure fort armok animals units This tool provides an interface for producing pregnancies with specific mothers -and fathers. +and fathers. It can also assign or unassign spouses. If a unit is selected when you run `gui/pregnancy`, they will be pre-selected as a parent. If the unit has a spouse of a different gender, they will be @@ -21,8 +21,14 @@ A unit must be on the map to participate in a pregnancy. For example, you cannot designate a father that is not on-site, even if they are the selected mother's spouse. -Children cannot be selected as a parent, and, due to game limitations, -cross-species pregnancies are not supported. +You can make the selected parents spouses by clicking on the "Set selected + as spouse" button, but the button is only enabled if you first +dissolve existing spouse relationships for both partners. If either new spouse +has existing lovers, you'll get a confirmation dialog, and if you choose to +proceed, the lover relationships will be removed. + +Children and units that are insane cannot be selected as a parent, and, due to +game limitations, cross-species pregnancies are not supported. Usage ----- @@ -30,3 +36,11 @@ Usage :: gui/pregnancy + +Technical notes +--------------- + +The reason for the requirement that a father must be on the map to contribute +to a pregnancy is that the genes used for the pregnancy are associated with the +physical unit. They are not stored with the "historical figure" that represents +the father when he is off-map. diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index dac365f502..a5d15a67a7 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -1,3 +1,4 @@ +local dlg = require('gui.dialogs') local gui = require('gui') local widgets = require('gui.widgets') @@ -7,7 +8,99 @@ local function zoom_to(unit) end local function is_viable_partner(unit, required_pronoun) - return unit and unit.sex == required_pronoun and dfhack.units.isAdult(unit) + return unit and unit.sex == required_pronoun and dfhack.units.isAdult(unit) and dfhack.units.isSane(unit) +end + +-- clears other_hf's link to hf (assumes there's only one reverse link) +local function clear_rev_hf_link(link_type, hfid, other_hfid) + local other_hf = df.historical_figure.find(other_hfid) + if not other_hf then return end + for i, link in ipairs(other_hf.histfig_links) do + if link._type == link_type and link.target_hf == hfid then + other_hf.histfig_links:erase(i) + link:delete() + break + end + end +end + +local function clear_hf_links(link_type, hfid) + local hf = df.historical_figure.find(hfid) + if not hf then return end + for i = #hf.histfig_links-1,0,-1 do + local link = hf.histfig_links[i] + if link._type == link_type then + clear_rev_hf_link(link_type, hfid, link.target_hf) + hf.histfig_links:erase(i) + link:delete() + end + end +end + +local function has_hf_links(link_type, hfid) + local hf = df.historical_figure.find(hfid) + if not hf then return false end + for i = #hf.histfig_links-1,0,-1 do + local link = hf.histfig_links[i] + if link._type == link_type then return true end + end + return false +end + +local function clear_spouse(unit, noconfirm) + if not unit then return end + local function do_clear_spouse() + clear_hf_links(df.histfig_hf_link_spousest, unit.hist_figure_id) + local spouse = df.unit.find(unit.relationship_ids.Spouse) + if spouse then + spouse.relationship_ids.Spouse = -1 + end + unit.relationship_ids.Spouse = -1 + end + if noconfirm then + do_clear_spouse() + else + dlg.showYesNoPrompt('Clear spouse', + ('Really clear spouse for %s?'):format(dfhack.units.getReadableName(unit)), + COLOR_YELLOW, do_clear_spouse) + end +end + +-- adds a link to hf pointing to other_hf +local function add_hf_link(link_type, hfid, other_hfid) + local hf = df.historical_figure.find(hfid) + if not hf then return end + local link = link_type:new() + link.target_hf = other_hfid + link.link_strength = 100 + hf.histfig_links:insert('#', link) +end + +local function set_spouse(unit1, unit2) + local function do_set_spouse() + clear_spouse(unit1, true) + clear_spouse(unit2, true) + unit1.relationship_ids.Spouse = unit2.id + unit2.relationship_ids.Spouse = unit1.id + add_hf_link(df.histfig_hf_link_spousest, unit1.hist_figure_id, unit2.hist_figure_id) + add_hf_link(df.histfig_hf_link_spousest, unit2.hist_figure_id, unit1.hist_figure_id) + dfhack.gui.showAutoAnnouncement(df.announcement_type.MARRIAGE, xyz2pos(dfhack.units.getPosition(unit1)), + ('%s and %s have married!'):format(dfhack.TranslateName(unit1.name), dfhack.TranslateName(unit2.name)), + COLOR_LIGHTMAGENTA) + end + local unit1_has_lovers = has_hf_links(df.histfig_hf_link_loverst, unit1.hist_figure_id) + local unit2_has_lovers = has_hf_links(df.histfig_hf_link_loverst, unit2.hist_figure_id) + if unit1_has_lovers or unit2_has_lovers then + dlg.showYesNoPrompt('Clear lovers', + 'New partners have existing lovers. Spurn them?', + COLOR_YELLOW, function() + clear_hf_links(df.histfig_hf_link_loverst, unit1.hist_figure_id) + clear_hf_links(df.histfig_hf_link_loverst, unit2.hist_figure_id) + do_set_spouse() + end) + else + do_set_spouse() + end end ---------------------- @@ -16,8 +109,9 @@ end Pregnancy = defclass(Pregnancy, widgets.Window) Pregnancy.ATTRS { - frame_title='Pregnancy manager', - frame={w=50, h=28, r=2, t=18}, + frame_title='Pregnancy and family manager', + frame={w=50, h=29, r=2, t=18}, + frame_inset={t=1, l=1, r=1}, resizable=true, } @@ -27,203 +121,272 @@ function Pregnancy:init() self.dirty = 0 self:addviews{ - widgets.Label{ - frame={t=0, l=0}, - text='Mother:', - }, - widgets.Label{ - frame={t=0, l=8}, - text='None (please select an adult female)', - text_pen=COLOR_YELLOW, - visible=function() return not self:get_mother() end, - }, - widgets.Label{ - frame={t=0, l=8}, - text={{text=self:callback('get_name', 'mother')}}, - text_pen=COLOR_LIGHTMAGENTA, - auto_width=true, - on_click=function() zoom_to(self:get_mother()) end, - visible=self:callback('get_mother'), - }, - widgets.Label{ - frame={t=1, l=0}, - text={{text=self:callback('get_pregnancy_desc')}}, - }, - widgets.Label{ - frame={t=3, l=0}, - text='Spouse:', - }, - widgets.Label{ - frame={t=3, l=8}, - text='None', - visible=function() return not self:get_spouse_unit('mother') and not self:get_spouse_hf('mother') end, - }, - widgets.Label{ - frame={t=3, l=8}, - text={{text=self:callback('get_spouse_name', 'mother')}}, - text_pen=COLOR_BLUE, - auto_width=true, - on_click=function() zoom_to(self:get_spouse_unit('mother')) end, - visible=self:callback('get_spouse_unit', 'mother'), - }, - widgets.Label{ - frame={t=3, l=8}, - text={ - {text=self:callback('get_spouse_hf_name', 'mother')}, - ' (off-site)', + widgets.Panel{ + frame={t=0}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text='Mother:', + }, + widgets.Label{ + frame={t=0, l=8}, + text='None (please select an adult female)', + text_pen=COLOR_YELLOW, + visible=function() return not self:get_mother() end, + }, + widgets.Label{ + frame={t=0, l=8}, + text={{text=self:callback('get_name', 'mother')}}, + text_pen=COLOR_LIGHTMAGENTA, + auto_width=true, + on_click=function() zoom_to(self:get_mother()) end, + visible=self:callback('get_mother'), + }, + widgets.Label{ + frame={t=1, l=0}, + text={{text=self:callback('get_pregnancy_desc')}}, + }, + widgets.Label{ + frame={t=3, l=0}, + text='Spouse:', + }, + widgets.Label{ + frame={t=3, l=8}, + text='None', + visible=function() return not self:get_spouse_unit('mother') and not self:get_spouse_hf('mother') end, + }, + widgets.Label{ + frame={t=3, l=8}, + text={{text=self:callback('get_spouse_name', 'mother')}}, + text_pen=COLOR_BLUE, + auto_width=true, + on_click=function() zoom_to(self:get_spouse_unit('mother')) end, + visible=self:callback('get_spouse_unit', 'mother'), + }, + widgets.Label{ + frame={t=3, l=8}, + text={ + {text=self:callback('get_spouse_hf_name', 'mother')}, + ' (off-site)', + }, + text_pen=COLOR_BLUE, + auto_width=true, + visible=function() return not self:get_spouse_unit('mother') and self:get_spouse_hf('mother') end, + }, + widgets.HotkeyLabel{ + frame={t=4, l=2}, + label="Set mother's spouse as the father", + key='CUSTOM_F', + auto_width=true, + on_activate=function() self:set_father(self:get_spouse_unit('mother')) end, + enabled=function() + local spouse = self:get_spouse_unit('mother') + return spouse and spouse.id ~= self.father_id and is_viable_partner(spouse, df.pronoun_type.he) + end, + }, + widgets.HotkeyLabel{ + frame={t=5, l=2}, + label="Dissolve spouse relationship", + key='CUSTOM_X', + auto_width=true, + on_activate=function() clear_spouse(self:get_mother()) self.dirty = 2 end, + visible=function() + local mother = self:get_mother() + return mother and mother.relationship_ids.Spouse ~= -1 + end, + }, + widgets.HotkeyLabel{ + frame={t=5, l=2}, + label="Set selected father as spouse", + key='CUSTOM_X', + auto_width=true, + on_activate=function() set_spouse(self:get_mother(), self:get_father()) self.dirty = 2 end, + visible=function() + local mother = self:get_mother() + return not mother or mother.relationship_ids.Spouse == -1 + end, + enabled=function() + if self.mother_id == -1 then return false end + local father = self:get_father() + return father and father.relationship_ids.Spouse == -1 + end, + }, + widgets.HotkeyLabel{ + frame={t=7, l=0}, + label="Choose selected unit to be the mother", + key='CUSTOM_SHIFT_M', + auto_width=true, + on_activate=self:callback('set_mother'), + enabled=function() + local unit = dfhack.gui.getSelectedUnit(true) + return unit and unit.id ~= self.mother_id and is_viable_partner(unit, df.pronoun_type.she) + end, + }, }, - text_pen=COLOR_BLUE, - auto_width=true, - visible=function() return not self:get_spouse_unit('mother') and self:get_spouse_hf('mother') end, - }, - widgets.HotkeyLabel{ - frame={t=4, l=2}, - label="Set mother's spouse as the father", - key='CUSTOM_F', - auto_width=true, - on_activate=function() self:set_father(self:get_spouse_unit('mother')) end, - enabled=function() - local spouse = self:get_spouse_unit('mother') - return spouse and spouse.id ~= self.father_id and is_viable_partner(spouse, df.pronoun_type.he) - end, - }, - widgets.HotkeyLabel{ - frame={t=6, l=0}, - label="Set mother to selected unit", - key='CUSTOM_SHIFT_M', - auto_width=true, - on_activate=self:callback('set_mother'), - enabled=function() - local unit = dfhack.gui.getSelectedUnit(true) - return unit and unit.id ~= self.mother_id and is_viable_partner(unit, df.pronoun_type.she) - end, }, widgets.Divider{ - frame={t=8, h=1}, + frame={t=9, h=1}, frame_style=gui.FRAME_THIN, frame_style_l=false, frame_style_r=false, }, - widgets.Label{ - frame={t=10, l=0}, - text='Father:', - }, - widgets.Label{ - frame={t=10, l=8}, - text={ - 'None ', - {text='(optionally select an adult male)', pen=COLOR_GRAY}, - }, - visible=function() return not self:get_father() end, - }, - widgets.Label{ - frame={t=10, l=8}, - text={{text=self:callback('get_name', 'father')}}, - text_pen=function() - local spouse = self:get_spouse_unit('mother') - if spouse and self.father_id == spouse.id then - return COLOR_BLUE - end - return COLOR_CYAN - end, - auto_width=true, - on_click=function() zoom_to(self:get_father()) end, - visible=self:callback('get_father'), - }, - widgets.Label{ - frame={t=12, l=0}, - text='Spouse:', - }, - widgets.Label{ - frame={t=12, l=8}, - text='None', - visible=function() return not self:get_spouse_unit('father') and not self:get_spouse_hf('father') end, - }, - widgets.Label{ - frame={t=12, l=8}, - text={{text=self:callback('get_spouse_name', 'father')}}, - text_pen=function() - local spouse = self:get_spouse_unit('father') - if spouse and self.mother_id == spouse.id then - return COLOR_LIGHTMAGENTA - end - return COLOR_CYAN - end, - auto_width=true, - on_click=function() zoom_to(self:get_spouse_unit('father')) end, - visible=self:callback('get_spouse_unit', 'father'), - }, - widgets.Label{ - frame={t=12, l=8}, - text={ - {text=self:callback('get_spouse_hf_name', 'father')}, - ' (off-site)', + widgets.Panel{ + frame={t=11}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text='Father:', + }, + widgets.Label{ + frame={t=0, l=8}, + text={ + 'None ', + {text='(optionally select an adult male)', pen=COLOR_GRAY}, + }, + visible=function() return not self:get_father() end, + }, + widgets.Label{ + frame={t=0, l=8}, + text={{text=self:callback('get_name', 'father')}}, + text_pen=function() + local spouse = self:get_spouse_unit('mother') + if spouse and self.father_id == spouse.id then + return COLOR_BLUE + end + return COLOR_CYAN + end, + auto_width=true, + on_click=function() zoom_to(self:get_father()) end, + visible=self:callback('get_father'), + }, + widgets.Label{ + frame={t=2, l=0}, + text='Spouse:', + }, + widgets.Label{ + frame={t=2, l=8}, + text='None', + visible=function() return not self:get_spouse_unit('father') and not self:get_spouse_hf('father') end, + }, + widgets.Label{ + frame={t=2, l=8}, + text={{text=self:callback('get_spouse_name', 'father')}}, + text_pen=function() + local spouse = self:get_spouse_unit('father') + if spouse and self.mother_id == spouse.id then + return COLOR_LIGHTMAGENTA + end + return COLOR_CYAN + end, + auto_width=true, + on_click=function() zoom_to(self:get_spouse_unit('father')) end, + visible=self:callback('get_spouse_unit', 'father'), + }, + widgets.Label{ + frame={t=2, l=8}, + text={ + {text=self:callback('get_spouse_hf_name', 'father')}, + ' (off-site)', + }, + text_pen=COLOR_CYAN, + auto_width=true, + visible=function() return not self:get_spouse_unit('father') and self:get_spouse_hf('father') end, + }, + widgets.HotkeyLabel{ + frame={t=3, l=2}, + label="Set father's spouse as the mother", + key='CUSTOM_M', + auto_width=true, + on_activate=function() self:set_mother(self:get_spouse_unit('father')) end, + enabled=function() + local spouse = self:get_spouse_unit('father') + return spouse and spouse.id ~= self.mother_id and is_viable_partner(spouse, df.pronoun_type.she) + end, + }, + widgets.HotkeyLabel{ + frame={t=4, l=2}, + label="Dissolve spouse relationship", + key='CUSTOM_SHIFT_X', + auto_width=true, + on_activate=function() clear_spouse(self:get_father()) self.dirty = 2 end, + visible=function() + local father = self:get_father() + return father and father.relationship_ids.Spouse ~= -1 + end, + }, + widgets.HotkeyLabel{ + frame={t=4, l=2}, + label="Set selected mother as spouse", + key='CUSTOM_SHIFT_X', + auto_width=true, + on_activate=function() set_spouse(self:get_mother(), self:get_father()) self.dirty = 2 end, + visible=function() + local father = self:get_father() + return not father or father.relationship_ids.Spouse == -1 + end, + enabled=function() + if self.father_id == -1 then return false end + local mother = self:get_mother() + return mother and mother.relationship_ids.Spouse == -1 + end, + }, + widgets.HotkeyLabel{ + frame={t=6, l=0}, + label="Choose selected unit to be the father", + key='CUSTOM_SHIFT_F', + auto_width=true, + on_activate=self:callback('set_father'), + enabled=function() + local unit = dfhack.gui.getSelectedUnit(true) + return unit and unit.id ~= self.father_id and is_viable_partner(unit, df.pronoun_type.he) + end, + }, }, - text_pen=COLOR_CYAN, - auto_width=true, - visible=function() return not self:get_spouse_unit('father') and self:get_spouse_hf('father') end, - }, - widgets.HotkeyLabel{ - frame={t=13, l=2}, - label="Set father's spouse as the mother", - key='CUSTOM_M', - auto_width=true, - on_activate=function() self:set_mother(self:get_spouse_unit('father')) end, - enabled=function() - local spouse = self:get_spouse_unit('father') - return spouse and spouse.id ~= self.mother_id and is_viable_partner(spouse, df.pronoun_type.she) - end, - }, - widgets.HotkeyLabel{ - frame={t=15, l=0}, - label="Set father to selected unit", - key='CUSTOM_SHIFT_F', - auto_width=true, - on_activate=self:callback('set_father'), - enabled=function() - local unit = dfhack.gui.getSelectedUnit(true) - return unit and unit.id ~= self.father_id and is_viable_partner(unit, df.pronoun_type.he) - end, }, widgets.Divider{ - frame={t=17, h=1}, + frame={t=19, h=1}, frame_style=gui.FRAME_THIN, frame_style_l=false, frame_style_r=false, }, - widgets.CycleHotkeyLabel{ - view_id='term', - frame={t=19, l=0, w=40}, - label='Pregnancy term (in months):', - key_back='CUSTOM_SHIFT_Z', - key='CUSTOM_Z', - options={ - {label='Default', value='default', pen=COLOR_BROWN}, - {label='0', value=0, pen=COLOR_BROWN}, - {label='1', value=1, pen=COLOR_BROWN}, - {label='2', value=2, pen=COLOR_BROWN}, - {label='3', value=3, pen=COLOR_BROWN}, - {label='4', value=4, pen=COLOR_BROWN}, - {label='5', value=5, pen=COLOR_BROWN}, - {label='6', value=6, pen=COLOR_BROWN}, - {label='7', value=7, pen=COLOR_BROWN}, - {label='8', value=8, pen=COLOR_BROWN}, - {label='9', value=9, pen=COLOR_BROWN}, - {label='10', value=10, pen=COLOR_BROWN}, - }, - initial_option='default', - }, widgets.Panel{ - frame={t=21, w=23, h=3}, - frame_style=gui.FRAME_INTERIOR, + frame={t=21}, subviews={ - widgets.HotkeyLabel{ - key='CUSTOM_SHIFT_P', - label="Generate pregnancy", - on_activate=self:callback('commit'), - enabled=function() return self:get_mother() end, + widgets.CycleHotkeyLabel{ + view_id='term', + frame={t=0, l=0, w=40}, + label='Pregnancy term (in months):', + key_back='CUSTOM_SHIFT_Z', + key='CUSTOM_Z', + options={ + {label='Default', value='default', pen=COLOR_BROWN}, + {label='0', value=0, pen=COLOR_BROWN}, + {label='1', value=1, pen=COLOR_BROWN}, + {label='2', value=2, pen=COLOR_BROWN}, + {label='3', value=3, pen=COLOR_BROWN}, + {label='4', value=4, pen=COLOR_BROWN}, + {label='5', value=5, pen=COLOR_BROWN}, + {label='6', value=6, pen=COLOR_BROWN}, + {label='7', value=7, pen=COLOR_BROWN}, + {label='8', value=8, pen=COLOR_BROWN}, + {label='9', value=9, pen=COLOR_BROWN}, + {label='10', value=10, pen=COLOR_BROWN}, + }, + initial_option='default', + }, + widgets.Panel{ + frame={t=2, w=23, h=3}, + frame_style=gui.FRAME_INTERIOR, + subviews={ + widgets.HotkeyLabel{ + key='CUSTOM_SHIFT_P', + label="Generate pregnancy", + on_activate=self:callback('commit'), + enabled=function() return self:get_mother() end, + }, + } }, - } + }, }, } @@ -317,33 +480,55 @@ end function Pregnancy:set_mother(unit) unit = unit or dfhack.gui.getSelectedUnit(true) if not is_viable_partner(unit, df.pronoun_type.she) then return end - self.mother_id = unit.id - if self.father_id ~= -1 then - local father = self:get_father() - if not father or father.race ~= unit.race then - self.father_id = -1 + local father = self:get_father() + local function do_set_mother() + if self.father_id ~= -1 then + if not father or father.race ~= unit.race then + self.father_id = -1 + end + end + self.mother_id = unit.id + if self.father_id == -1 then + self:set_father(self:get_spouse_unit('mother')) end + self.dirty = 2 end - if self.father_id == -1 then - self:set_father(self:get_spouse_unit('mother')) + if father and father.race ~= unit.race then + dlg.showYesNoPrompt('Race mismatch', + 'Are you sure you want to select this unit as the mother?\n' .. + 'The unit\'s race does not match the selected father.\n' .. + 'The choice for father will be reset.', + COLOR_YELLOW, do_set_mother) + else + do_set_mother() end - self.dirty = 2 end function Pregnancy:set_father(unit) unit = unit or dfhack.gui.getSelectedUnit(true) if not is_viable_partner(unit, df.pronoun_type.he) then return end - self.father_id = unit.id - if self.mother_id ~= -1 then - local mother = self:get_mother() - if not mother or mother.race ~= unit.race then - self.mother_id = -1 + local mother = self:get_mother() + local function do_set_father() + if self.mother_id ~= -1 then + if not mother or mother.race ~= unit.race then + self.mother_id = -1 + end + end + self.father_id = unit.id + if self.mother_id == -1 then + self:set_mother(self:get_spouse_unit('father')) end + self.dirty = 2 end - if self.mother_id == -1 then - self:set_mother(self:get_spouse_unit('father')) + if mother and mother.race ~= unit.race then + dlg.showYesNoPrompt('Race mismatch', + 'Are you sure you want to select this unit as the father?\n' .. + 'The unit\'s race does not match the selected mother.\n' .. + 'The choice for mother will be reset.', + COLOR_YELLOW, do_set_father) + else + do_set_father() end - self.dirty = 2 end local function get_term_ticks(months) From e5f64989293d06eca4b5a82b4374191660a4d891 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 23 Aug 2024 02:29:47 -0700 Subject: [PATCH 003/811] remove gui/family-affairs functionality merged into gui/pregnancy --- changelog.txt | 1 + docs/gui/family-affairs.rst | 29 ---- gui/family-affairs.lua | 263 ------------------------------------ 3 files changed, 1 insertion(+), 292 deletions(-) delete mode 100644 docs/gui/family-affairs.rst delete mode 100644 gui/family-affairs.lua diff --git a/changelog.txt b/changelog.txt index 2c84dd719c..c17b89fb3f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -45,6 +45,7 @@ Template for new versions: - `gui/sitemap`: show whether a unit is caged ## Removed +- `gui/family-affairs`: merged into `gui/pregnancy` # 50.13-r4 diff --git a/docs/gui/family-affairs.rst b/docs/gui/family-affairs.rst deleted file mode 100644 index 7a1e0a4bfc..0000000000 --- a/docs/gui/family-affairs.rst +++ /dev/null @@ -1,29 +0,0 @@ -gui/family-affairs -================== - -.. dfhack-tool:: - :summary: Inspect or meddle with romantic relationships. - :tags: unavailable - -This tool provides a user-friendly interface to view romantic relationships, -with the ability to add, remove, or otherwise change them at your whim - -fantastic for depressed dwarves with a dead spouse (or matchmaking players...). - -The target/s must be alive, sane, and in fortress mode. - -Usage ------ - -``gui/family-affairs [unitID]`` - Show GUI for the selected unit, or the unit with the specified unit ID. -``gui/family-affairs divorce [unitID]`` - Remove all spouse and lover information from the unit and their partner. -``gui/family-affairs [unitID] [unitID]`` - Divorce the two specified units and their partners, then arrange for the two - units to marry. - -Screenshot ----------- - -.. image:: /docs/images/family-affairs.png - :align: center diff --git a/gui/family-affairs.lua b/gui/family-affairs.lua deleted file mode 100644 index 1dd08b8475..0000000000 --- a/gui/family-affairs.lua +++ /dev/null @@ -1,263 +0,0 @@ --- gui/family-affairs --- derived from v1.2 @ http://www.bay12forums.com/smf/index.php?topic=147779 - -local dlg = require ('gui.dialogs') - -function ErrorPopup (msg,color) - if not tostring(msg) then msg = "Error" end - if not color then color = COLOR_LIGHTRED end - dlg.showMessage("Dwarven Family Affairs", msg, color, nil) -end - -function AnnounceAndGamelog(text) - dfhack.gui.showAnnouncement(text, COLOR_LIGHTMAGENTA) -end - -function ListPrompt (msg, choicelist, bool, yes_func) -dlg.showListPrompt( - "Dwarven Family Affairs", - msg, - COLOR_WHITE, - choicelist, - --called if choice is yes - yes_func, - --called on cancel - function() end, - 15, - bool - ) -end - -function GetMarriageSummary (source) - local familystate = "" - local source_name = dfhack.TranslateName(source.name) - - local spouse = df.unit.find(source.relationship_ids.Spouse) - local lover = df.unit.find(source.relationship_ids.Lover) - - if spouse then - if dfhack.units.isSane(spouse) then - familystate = source_name.." has a spouse ("..dfhack.TranslateName(spouse.name)..")" - end - if dfhack.units.isSane(spouse) == false then - familystate = source_name.."'s spouse is dead or not sane, would you like to choose a new one?" - end - elseif lover then - if dfhack.units.isSane(df.unit.find(source.relationship_ids.Lover)) then - familystate = source_name.." already has a lover ("..dfhack.TranslateName(lover.name)..")" - end - if dfhack.units.isSane(df.unit.find(source.relationship_ids.Lover)) == false then - familystate = source_name.."'s lover is dead or not sane, would you like that love forgotten?" - end - else - familystate = source_name.." is not involved in romantic relationships with anyone" - end - - if source.pregnancy_timer > 0 then - familystate = familystate.."\nShe is pregnant." - local father = df.historical_figure.find(source.pregnancy_spouse) - if father then - familystate = familystate.." The father is "..dfhack.TranslateName(father.name).."." - end - end - - return familystate -end - -function GetSpouseData (source) - local spouse = df.unit.find(source.relationship_ids.Spouse) - local spouse_hf - if spouse then - spouse_hf = df.historical_figure.find(spouse.hist_figure_id) - end - return spouse,spouse_hf -end - -function GetLoverData (source) - local lover = df.unit.find(source.relationship_ids.Spouse) - local lover_hf - if lover then - lover_hf = df.historical_figure.find (lover.hist_figure_id) - end - return lover,lover_hf -end - -function EraseHFLinksLoverSpouse (hf) - for i = #hf.histfig_links-1,0,-1 do - if hf.histfig_links[i]._type == df.histfig_hf_link_spousest or hf.histfig_links[i]._type == df.histfig_hf_link_loverst then - local todelete = hf.histfig_links[i] - hf.histfig_links:erase(i) - todelete:delete() - end - end -end - -function Divorce (source) - local source_hf = df.historical_figure.find(source.hist_figure_id) - local spouse,spouse_hf = GetSpouseData (source) - local lover,lover_hf = GetLoverData (source) - - source.relationship_ids.Spouse = -1 - source.relationship_ids.Lover = -1 - - if source_hf then - EraseHFLinksLoverSpouse (source_hf) - end - if spouse then - spouse.relationship_ids.Spouse = -1 - spouse.relationship_ids.Lover = -1 - end - if lover then - spouse.relationship_ids.Spouse = -1 - spouse.relationship_ids.Lover = -1 - end - if spouse_hf then - EraseHFLinksLoverSpouse (spouse_hf) - end - if lover_hf then - EraseHFLinksLoverSpouse (lover_hf) - end - - local partner = spouse or lover - if not partner then - AnnounceAndGamelog(dfhack.TranslateName(source.name).." is now single") - else - AnnounceAndGamelog(dfhack.TranslateName(source.name).." and "..dfhack.TranslateName(partner.name).." are now single") - end -end - -function Marriage (source,target) - local source_hf = df.historical_figure.find(source.hist_figure_id) - local target_hf = df.historical_figure.find(target.hist_figure_id) - source.relationship_ids.Spouse = target.id - target.relationship_ids.Spouse = source.id - - local new_link = df.histfig_hf_link_spousest:new() -- adding hf link to source - new_link.target_hf = target_hf.id - new_link.link_strength = 100 - source_hf.histfig_links:insert('#',new_link) - - new_link = df.histfig_hf_link_spousest:new() -- adding hf link to target - new_link.target_hf = source_hf.id - new_link.link_strength = 100 - target_hf.histfig_links:insert('#',new_link) -end - -function ChooseNewSpouse (source) - - if not source then - qerror("no unit") return - end - if not dfhack.units.isAdult(source) then - ErrorPopup("target is too young") return - end - if not (source.relationship_ids.Spouse == -1 and source.relationship_ids.Lover == -1) then - ErrorPopup("target already has a spouse or a lover") - qerror("source already has a spouse or a lover") - return - end - - local choicelist = {} - targetlist = {} - - for k,v in pairs (dfhack.units.getCitizens()) do - if v.race == source.race - and v.sex ~= source.sex - and v.relationship_ids.Spouse == -1 - and v.relationship_ids.Lover == -1 - and dfhack.units.isAdult(v) - then - table.insert(choicelist,dfhack.TranslateName(v.name)..', '..dfhack.units.getProfessionName(v)) - table.insert(targetlist,v) - end - end - - if #choicelist > 0 then - ListPrompt( - "Assign new spouse for "..dfhack.TranslateName(source.name), - choicelist, - true, - function(a,b) - local target = targetlist[a] - Marriage (source,target) - AnnounceAndGamelog(dfhack.TranslateName(source.name).." and "..dfhack.TranslateName(target.name).." have married!") - end) - else - ErrorPopup("No suitable candidates") - end -end - -function MainDialog (source) - - local familystate = GetMarriageSummary(source) - - familystate = familystate.."\nSelect action:" - local choicelist = {} - local on_select = {} - - local adult = dfhack.units.isAdult(source) - local single = source.relationship_ids.Spouse == -1 and source.relationship_ids.Lover == -1 - local ready_for_marriage = single and adult - - if adult then - table.insert(choicelist,"Remove romantic relationships (if any)") - table.insert(on_select, Divorce) - if ready_for_marriage then - table.insert(choicelist,"Assign a new spouse") - table.insert(on_select,ChooseNewSpouse) - end - if not ready_for_marriage then - table.insert(choicelist,"[Assign a new spouse]") - table.insert(on_select,function () ErrorPopup ("Existing relationships must be removed if you wish to assign a new spouse.") end) - end - else - table.insert(choicelist,"Leave this child alone") - table.insert(on_select,nil) - end - - ListPrompt(familystate, choicelist, false, - function(a,b) if on_select[a] then on_select[a](source) end end) -end - - -local args = {...} - -if args[1] == "help" or args[1] == "?" then print(dfhack.script_help()) return end - -if not dfhack.world.isFortressMode() then - print(dfhack.script_help()) - qerror("invalid game mode") return -end - -if args[1] == "divorce" and tonumber(args[2]) then - local unit = df.unit.find(tonumber(args[2])) - if unit then Divorce (unit) return end -end - -if tonumber(args[1]) and tonumber(args[2]) then - local unit1 = df.unit.find(tonumber(args[1])) - local unit2 = df.unit.find(tonumber(args[2])) - if unit1 and unit2 then - Divorce (unit1) - Divorce (unit2) - Marriage (unit1,unit2) - return - end -end - -local selected = dfhack.gui.getSelectedUnit(true) -if tonumber(args[1]) then - selected = df.unit.find(tonumber(args[1])) or selected -end - -if selected then - if dfhack.units.isCitizen(selected) then - MainDialog(selected) - else - qerror("You must select a sane fortress citizen.") - return - end -else - print(dfhack.script_help()) - qerror("Select a sane fortress dwarf") -end From 35516d675f5bffc1abcc929b1b78db45ce79035c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 23 Aug 2024 03:25:12 -0700 Subject: [PATCH 004/811] don't allow player to assign spouses for non-spouse races --- gui/pregnancy.lua | 48 +++++++++++++++++++++++++++++++---------------- 1 file changed, 32 insertions(+), 16 deletions(-) diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index a5d15a67a7..733085ab70 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -11,6 +11,12 @@ local function is_viable_partner(unit, required_pronoun) return unit and unit.sex == required_pronoun and dfhack.units.isAdult(unit) and dfhack.units.isSane(unit) end +local function can_have_spouse(unit) + if not unit then return end + local caste_flags = unit.enemy.caste_flags + return caste_flags.CAN_SPEAK or caste_flags.CAN_LEARN +end + -- clears other_hf's link to hf (assumes there's only one reverse link) local function clear_rev_hf_link(link_type, hfid, other_hfid) local other_hf = df.historical_figure.find(other_hfid) @@ -148,11 +154,11 @@ function Pregnancy:init() }, widgets.Label{ frame={t=3, l=0}, - text='Spouse:', + text={{text='Spouse:', pen=function() return can_have_spouse(self:get_mother()) and COLOR_WHITE or COLOR_GRAY end}}, }, widgets.Label{ frame={t=3, l=8}, - text='None', + text={{text='None', pen=function() return can_have_spouse(self:get_mother()) and COLOR_WHITE or COLOR_GRAY end}}, visible=function() return not self:get_spouse_unit('mother') and not self:get_spouse_hf('mother') end, }, widgets.Label{ @@ -166,10 +172,9 @@ function Pregnancy:init() widgets.Label{ frame={t=3, l=8}, text={ - {text=self:callback('get_spouse_hf_name', 'mother')}, - ' (off-site)', + {text=self:callback('get_spouse_hf_name', 'mother'), pen=COLOR_BLUE}, + {gap=1, text='(off-site)', pen=COLOR_YELLOW}, }, - text_pen=COLOR_BLUE, auto_width=true, visible=function() return not self:get_spouse_unit('mother') and self:get_spouse_hf('mother') end, }, @@ -189,7 +194,10 @@ function Pregnancy:init() label="Dissolve spouse relationship", key='CUSTOM_X', auto_width=true, - on_activate=function() clear_spouse(self:get_mother()) self.dirty = 2 end, + on_activate=function() + clear_spouse(self:get_mother()) + self.dirty = 3 + end, visible=function() local mother = self:get_mother() return mother and mother.relationship_ids.Spouse ~= -1 @@ -200,13 +208,16 @@ function Pregnancy:init() label="Set selected father as spouse", key='CUSTOM_X', auto_width=true, - on_activate=function() set_spouse(self:get_mother(), self:get_father()) self.dirty = 2 end, + on_activate=function() + set_spouse(self:get_mother(), self:get_father()) + self.dirty = 3 + end, visible=function() local mother = self:get_mother() return not mother or mother.relationship_ids.Spouse == -1 end, enabled=function() - if self.mother_id == -1 then return false end + if not can_have_spouse(self:get_mother()) then return false end local father = self:get_father() return father and father.relationship_ids.Spouse == -1 end, @@ -261,11 +272,11 @@ function Pregnancy:init() }, widgets.Label{ frame={t=2, l=0}, - text='Spouse:', + text={{text='Spouse:', pen=function() return can_have_spouse(self:get_father()) and COLOR_WHITE or COLOR_GRAY end}}, }, widgets.Label{ frame={t=2, l=8}, - text='None', + text={{text='None', pen=function() return can_have_spouse(self:get_father()) and COLOR_WHITE or COLOR_GRAY end}}, visible=function() return not self:get_spouse_unit('father') and not self:get_spouse_hf('father') end, }, widgets.Label{ @@ -285,10 +296,9 @@ function Pregnancy:init() widgets.Label{ frame={t=2, l=8}, text={ - {text=self:callback('get_spouse_hf_name', 'father')}, - ' (off-site)', + {text=self:callback('get_spouse_hf_name', 'father'), pen=COLOR_CYAN}, + {gap=1, text='(off-site)', pen=COLOR_YELLOW}, }, - text_pen=COLOR_CYAN, auto_width=true, visible=function() return not self:get_spouse_unit('father') and self:get_spouse_hf('father') end, }, @@ -308,7 +318,10 @@ function Pregnancy:init() label="Dissolve spouse relationship", key='CUSTOM_SHIFT_X', auto_width=true, - on_activate=function() clear_spouse(self:get_father()) self.dirty = 2 end, + on_activate=function() + clear_spouse(self:get_father()) + self.dirty = 3 + end, visible=function() local father = self:get_father() return father and father.relationship_ids.Spouse ~= -1 @@ -319,13 +332,16 @@ function Pregnancy:init() label="Set selected mother as spouse", key='CUSTOM_SHIFT_X', auto_width=true, - on_activate=function() set_spouse(self:get_mother(), self:get_father()) self.dirty = 2 end, + on_activate=function() + set_spouse(self:get_mother(), self:get_father()) + self.dirty = 3 + end, visible=function() local father = self:get_father() return not father or father.relationship_ids.Spouse == -1 end, enabled=function() - if self.father_id == -1 then return false end + if not can_have_spouse(self:get_father()) then return false end local mother = self:get_mother() return mother and mother.relationship_ids.Spouse == -1 end, From 6dd7928765e16ac0fc5104db058806ba4fffba8a Mon Sep 17 00:00:00 2001 From: John Fisher Date: Fri, 23 Aug 2024 15:13:25 -0400 Subject: [PATCH 005/811] [gui/design] Update Line & Freeform tools to not overcount tiles --- internal/design/shapes.lua | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index 6db26c527b..0007dece39 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -445,8 +445,10 @@ function Line:plot_bresenham(x0, y0, x1, y1, thickness) while true do for j = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do if not self.arr[x + j] then self.arr[x + j] = {} end - self.arr[x + j][y] = true - self.num_tiles = self.num_tiles + 1 + if not self.arr[x + j][y] then + self.arr[x + j][y] = true + self.num_tiles = self.num_tiles + 1 + end end if x == x1 and y == y1 + i then @@ -641,8 +643,10 @@ function FreeForm:update(points, extra_points) for x, y_row in pairs(line_class.arr) do for y, _ in pairs(y_row) do if not self.arr[x] then self.arr[x] = {} end - self.arr[x][y] = true - self.num_tiles = self.num_tiles + 1 + if not self.arr[x][y] then + self.arr[x][y] = true + self.num_tiles = self.num_tiles + 1 + end end end end From 68d0aeac12fad17dd5d1db38eef8756925db9319 Mon Sep 17 00:00:00 2001 From: John Fisher Date: Fri, 23 Aug 2024 15:26:57 -0400 Subject: [PATCH 006/811] fix whitespace --- internal/design/shapes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index 0007dece39..5199aa1d10 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -643,7 +643,7 @@ function FreeForm:update(points, extra_points) for x, y_row in pairs(line_class.arr) do for y, _ in pairs(y_row) do if not self.arr[x] then self.arr[x] = {} end - if not self.arr[x][y] then + if not self.arr[x][y] then self.arr[x][y] = true self.num_tiles = self.num_tiles + 1 end From b845dd4c21a7a69296afef35ca82dbfcbe9a48ca Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 23 Aug 2024 13:42:58 -0700 Subject: [PATCH 007/811] use correct test for whether units can marry --- gui/pregnancy.lua | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/gui/pregnancy.lua b/gui/pregnancy.lua index 733085ab70..c7f1dbbad6 100644 --- a/gui/pregnancy.lua +++ b/gui/pregnancy.lua @@ -14,7 +14,7 @@ end local function can_have_spouse(unit) if not unit then return end local caste_flags = unit.enemy.caste_flags - return caste_flags.CAN_SPEAK or caste_flags.CAN_LEARN + return caste_flags.CAN_LEARN and not caste_flags.SLOW_LEARNER end -- clears other_hf's link to hf (assumes there's only one reverse link) @@ -217,9 +217,10 @@ function Pregnancy:init() return not mother or mother.relationship_ids.Spouse == -1 end, enabled=function() - if not can_have_spouse(self:get_mother()) then return false end + local mother = self:get_mother() local father = self:get_father() - return father and father.relationship_ids.Spouse == -1 + return mother and mother.relationship_ids.Spouse == -1 and can_have_spouse(mother) and + father and father.relationship_ids.Spouse == -1 end, }, widgets.HotkeyLabel{ @@ -341,9 +342,10 @@ function Pregnancy:init() return not father or father.relationship_ids.Spouse == -1 end, enabled=function() - if not can_have_spouse(self:get_father()) then return false end local mother = self:get_mother() - return mother and mother.relationship_ids.Spouse == -1 + local father = self:get_father() + return mother and mother.relationship_ids.Spouse == -1 and can_have_spouse(mother) and + father and father.relationship_ids.Spouse == -1 end, }, widgets.HotkeyLabel{ From 3c6befdee4329738728ae062e81baa2154936533 Mon Sep 17 00:00:00 2001 From: John Fisher Date: Fri, 23 Aug 2024 17:18:34 -0400 Subject: [PATCH 008/811] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index de4595e256..1c305263a5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -39,6 +39,7 @@ Template for new versions: ## Fixes - `timestream`: ensure child growth events (e.g. becoming an adult) are not skipped over - `empty-bin`: ``--liquids`` option correctly emptying containers filled with LIQUID_MISC +- `gui/design`: Update Line & Freeform tools to not overcount tiles ## Misc Improvements - `gui/sitemap`: show whether a unit is friendly, hostile, or wildlife From 9dffdd1f7c95c7e17406d2b9c18fb6f2b3f866da Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 23 Aug 2024 17:20:32 -0700 Subject: [PATCH 009/811] rename gui/pregnancy to gui/family-affairs --- docs/gui/{pregnancy.rst => family-affairs.rst} | 0 gui/{pregnancy.lua => family-affairs.lua} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename docs/gui/{pregnancy.rst => family-affairs.rst} (100%) rename gui/{pregnancy.lua => family-affairs.lua} (100%) diff --git a/docs/gui/pregnancy.rst b/docs/gui/family-affairs.rst similarity index 100% rename from docs/gui/pregnancy.rst rename to docs/gui/family-affairs.rst diff --git a/gui/pregnancy.lua b/gui/family-affairs.lua similarity index 100% rename from gui/pregnancy.lua rename to gui/family-affairs.lua From 533373dab29ecc01bf26b706aef1a58b7e02731f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 24 Aug 2024 18:26:46 -0700 Subject: [PATCH 010/811] split into tabs --- docs/gui/family-affairs.rst | 49 +-- gui/family-affairs.lua | 597 ++++++++++++++++++++++++++---------- 2 files changed, 452 insertions(+), 194 deletions(-) diff --git a/docs/gui/family-affairs.rst b/docs/gui/family-affairs.rst index 334832a521..a6d0a039fd 100644 --- a/docs/gui/family-affairs.rst +++ b/docs/gui/family-affairs.rst @@ -1,41 +1,42 @@ -gui/pregnancy -============= +gui/family-affairs +================== .. dfhack-tool:: - :summary: Generate pregnancies with pairings of your choice. + :summary: Manage romantic relationships and generate pregnancies. :tags: adventure fort armok animals units -This tool provides an interface for producing pregnancies with specific mothers -and fathers. It can also assign or unassign spouses. +This tool provides an interface for inspecting (or meddling with) romantic +relationships and for producing pregnancies with specific mothers and fathers. +Perfect for matchmaking players! -If a unit is selected when you run `gui/pregnancy`, they will be pre-selected -as a parent. If the unit has a spouse of a different gender, they will be -automatically selected as the other parent. You can click on other units on the -map and choose them as alternate mothers or fathers as desired. +If a unit is selected when you run `gui/family-affairs`, they will be +pre-loaded as a romantic partner (or prospective parent). While the window is +up, you can click on units on the map and assign them roles in the +`gui/family-affairs` UI. -If a unit is selected as a mother or father, or is listed as a spouse, you can -zoom the map to their location by clicking on their name in the `gui/pregnancy` -UI. +You can click on unit names in the `gui/family-affairs` UI to zoom the map to +their location. -A unit must be on the map to participate in a pregnancy. For example, you -cannot designate a father that is not on-site, even if they are the selected -mother's spouse. +Whereas you can choose any historical figure (of the same race) to serve as a +spouse or lover, a unit must be on the map to participate in a pregnancy. For +example, you cannot generate a pregnancy with a father that is not on-site, +even if they are the selected mother's spouse. -You can make the selected parents spouses by clicking on the "Set selected - as spouse" button, but the button is only enabled if you first -dissolve existing spouse relationships for both partners. If either new spouse -has existing lovers, you'll get a confirmation dialog, and if you choose to -proceed, the lover relationships will be removed. - -Children and units that are insane cannot be selected as a parent, and, due to -game limitations, cross-species pregnancies are not supported. +Children and units that are insane cannot be selected for participation in a +pregnancy, and, due to game limitations, cross-species pregnancies are not +supported. Usage ----- :: - gui/pregnancy + gui/family-affairs + gui/family-affairs --pregnancy + +Passing the ``--pregnancy`` option will start the `gui/family-affairs` UI on +the "Pregnancies" tab, and any (adult, sane) unit you have selected at the time +will be pre-selected as a parent. Technical notes --------------- diff --git a/gui/family-affairs.lua b/gui/family-affairs.lua index c7f1dbbad6..6de9cbd108 100644 --- a/gui/family-affairs.lua +++ b/gui/family-affairs.lua @@ -2,12 +2,16 @@ local dlg = require('gui.dialogs') local gui = require('gui') local widgets = require('gui.widgets') +---------------------- +-- Utility functions +-- + local function zoom_to(unit) if not unit then return end dfhack.gui.revealInDwarfmodeMap(xyz2pos(dfhack.units.getPosition(unit)), true, true) end -local function is_viable_partner(unit, required_pronoun) +local function is_viable_parent(unit, required_pronoun) return unit and unit.sex == required_pronoun and dfhack.units.isAdult(unit) and dfhack.units.isSane(unit) end @@ -18,7 +22,7 @@ local function can_have_spouse(unit) end -- clears other_hf's link to hf (assumes there's only one reverse link) -local function clear_rev_hf_link(link_type, hfid, other_hfid) +local function remove_hf_link(link_type, hfid, other_hfid) local other_hf = df.historical_figure.find(other_hfid) if not other_hf then return end for i, link in ipairs(other_hf.histfig_links) do @@ -36,7 +40,7 @@ local function clear_hf_links(link_type, hfid) for i = #hf.histfig_links-1,0,-1 do local link = hf.histfig_links[i] if link._type == link_type then - clear_rev_hf_link(link_type, hfid, link.target_hf) + remove_hf_link(link_type, hfid, link.target_hf) hf.histfig_links:erase(i) link:delete() end @@ -53,6 +57,60 @@ local function has_hf_links(link_type, hfid) return false end +local function has_spouse_or_lover(unit, target) + local hf = df.historical_figure.find(unit.hist_figure_id) + if not hf then return false end + for _, link in ipairs(hf.histfig_links) do + if (link._type == df.histfig_hf_link_spousest or link._type == df.histfig_hf_link_loverst) and + (not target or link.target_hf == target.hist_figure_id) + then + return true + end + end +end + +local function get_lovers(unit) + local lovers = {} + if not unit then return lovers end + local hf = df.historical_figure.find(unit.hist_figure_id) + if not hf then return lovers end + for _, link in ipairs(hf.histfig_links) do + if link._type == df.histfig_hf_link_loverst then + table.insert(lovers, link.target_hf) + end + end + return lovers +end + +local function get_name(unit_or_hf) + return unit_or_hf and dfhack.units.getReadableName(unit_or_hf) or '' +end + +local function get_spouse_unit(unit) + if not unit then return end + return df.unit.find(unit.relationship_ids.Spouse) +end + +local function get_spouse_hf(unit) + if not unit or unit.relationship_ids.Spouse == -1 then + return + end + local spouse = df.unit.find(unit.relationship_ids.Spouse) + if spouse then + return df.historical_figure.find(spouse.hist_figure_id) + end + + local hf = df.historical_figure.find(unit.hist_figure_id) + if not hf then return end + + for _, link in ipairs(hf.histfig_links) do + if link._type == df.histfig_hf_link_spousest then + -- may be nil due to hf culling, but then we just treat it as not having a spouse + return df.historical_figure.find(link.target_hf) + end + end +end + local function clear_spouse(unit, noconfirm) if not unit then return end local function do_clear_spouse() @@ -67,7 +125,7 @@ local function clear_spouse(unit, noconfirm) do_clear_spouse() else dlg.showYesNoPrompt('Clear spouse', - ('Really clear spouse for %s?'):format(dfhack.units.getReadableName(unit)), + ('Really clear spouse for %s?'):format(get_name(unit)), COLOR_YELLOW, do_clear_spouse) end end @@ -110,18 +168,257 @@ local function set_spouse(unit1, unit2) end ---------------------- --- Pregnancy +-- RelationshipsPage -- -Pregnancy = defclass(Pregnancy, widgets.Window) -Pregnancy.ATTRS { - frame_title='Pregnancy and family manager', - frame={w=50, h=29, r=2, t=18}, - frame_inset={t=1, l=1, r=1}, - resizable=true, -} +RelationshipsPage = defclass(RelationshipsPage, widgets.Panel) + +function RelationshipsPage:init() + self.cache = {} + self.unit_id = -1 + self.dirty = 0 + + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text='Name:', + }, + widgets.Label{ + frame={t=0, l=6}, + text='None (please select a unit)', + text_pen=COLOR_YELLOW, + visible=function() return not self:get_unit() end, + }, + widgets.Label{ + frame={t=0, l=6}, + text={{text=self:callback('get_name')}}, + text_pen=COLOR_LIGHTMAGENTA, + auto_width=true, + on_click=function() zoom_to(self:get_unit()) end, + visible=self:callback('get_unit'), + }, + widgets.HotkeyLabel{ + frame={t=2, l=0}, + label="Inspect selected unit", + key='CUSTOM_U', + auto_width=true, + on_activate=self:callback('set_unit'), + enabled=function() + local unit = dfhack.gui.getSelectedUnit(true) + return unit and unit.id ~= self.unit_id + end, + }, + widgets.Label{ + frame={t=4, l=0}, + text={{text='Spouse:', pen=function() return can_have_spouse(self:get_unit()) and COLOR_WHITE or COLOR_GRAY end}}, + }, + widgets.Label{ + frame={t=4, l=8}, + text={{text='None', pen=function() return can_have_spouse(self:get_unit()) and COLOR_WHITE or COLOR_GRAY end}}, + visible=function() return not self:get_spouse_unit() and not get_spouse_hf(self:get_unit()) end, + }, + widgets.Label{ + frame={t=4, l=8}, + text={{text=function() return get_name(self:get_spouse_unit()) end}}, + text_pen=COLOR_BLUE, + auto_width=true, + on_click=function() zoom_to(self:get_spouse_unit()) end, + visible=self:callback('get_spouse_unit'), + }, + widgets.Label{ + frame={t=4, l=8}, + text={ + {text=function() return get_name(get_spouse_hf(self:get_unit())) end, pen=COLOR_BLUE}, + {gap=1, text='(off-site)', pen=COLOR_YELLOW}, + }, + auto_width=true, + visible=function() return not self:get_spouse_unit() and get_spouse_hf(self:get_unit()) end, + }, + widgets.HotkeyLabel{ + frame={t=5, l=2}, + label="Dissolve spouse relationship", + key='CUSTOM_X', + auto_width=true, + on_activate=function() + clear_spouse(self:get_unit()) + self.dirty = 2 + end, + visible=function() + local unit = self:get_unit() + return unit and unit.relationship_ids.Spouse ~= -1 + end, + }, + widgets.HotkeyLabel{ + frame={t=5, l=2}, + label="Set selected unit as spouse", + key='CUSTOM_X', + auto_width=true, + on_activate=function() + set_spouse(self:get_unit(), dfhack.gui.getSelectedUnit(true)) + self.dirty = 2 + end, + visible=function() + local unit = self:get_unit() + return not unit or unit.relationship_ids.Spouse == -1 + end, + enabled=function() + local unit = self:get_unit() + local selected = dfhack.gui.getSelectedUnit(true) + return unit and unit.relationship_ids.Spouse == -1 and can_have_spouse(unit) and + selected and selected.race == unit.race and selected.id ~= unit.id + end, + }, + widgets.Panel{ + frame={t=7, b=3}, + frame_style=gui.FRAME_INTERIOR, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Lovers:', + {gap=1, text=function() return #self.subviews.lovers:getChoices() end, pen=COLOR_YELLOW}, + }, + }, + widgets.List{ + frame={t=2, b=2}, + view_id='lovers', + on_submit=function(_, choice) + local hf = df.historical_figure.find(choice.data.hfid) + if not hf then return end + local unit = df.unit.find(hf.unit_id) + if not unit then return end + zoom_to(unit) + end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + label='Remove lover', + key='CUSTOM_SHIFT_L', + auto_width=true, + on_activate=function() + local _, selected = self.subviews.lovers:getSelected() + if not selected then return end + local unit = self:get_unit() + if not unit then return end + remove_hf_link(df.histfig_hf_link_loverst, unit.hist_figure_id, selected.data.hfid) + remove_hf_link(df.histfig_hf_link_loverst, selected.data.hfid, unit.hist_figure_id) + self.dirty = 1 + end, + enabled=function() + local _, selected = self.subviews.lovers:getSelected() + return selected + end, + }, + }, + }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + label='Add selected unit as spouse', + key='CUSTOM_S', + auto_width=true, + on_activate=function() + set_spouse(self:get_unit(), dfhack.gui.getSelectedUnit(true)) + self.dirty = 3 + end, + enabled=function() + local unit = self:get_unit() + local selected = dfhack.gui.getSelectedUnit(true) + return unit and selected and selected.race == unit.race and selected.id ~= unit.id and + not has_spouse_or_lover(unit, selected) + end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + label='Add selected unit as lover', + key='CUSTOM_L', + auto_width=true, + on_activate=function() + local selected = dfhack.gui.getSelectedUnit(true) + if not selected then return end + local unit = self:get_unit() + if not unit then return end + add_hf_link(df.histfig_hf_link_loverst, unit.hist_figure_id, selected.hist_figure_id) + add_hf_link(df.histfig_hf_link_loverst, selected.hist_figure_id, unit.hist_figure_id) + self.dirty = 1 + end, + enabled=function() + local unit = self:get_unit() + local selected = dfhack.gui.getSelectedUnit(true) + return unit and selected and selected.race == unit.race and selected.id ~= unit.id and + not has_spouse_or_lover(unit, selected) + end, + }, + } +end + +function RelationshipsPage:on_show() + local unit = dfhack.gui.getSelectedUnit(true) + if self.unit_id == -1 then + self:set_unit(unit) + end +end + +function RelationshipsPage:get_unit() + self.cache.unit = self.cache.unit or df.unit.find(self.unit_id) + return self.cache.unit +end + +function RelationshipsPage:set_unit(unit) + unit = unit or dfhack.gui.getSelectedUnit(true) + if not unit then return end + self.unit_id = unit.id + self.dirty = 2 +end + +function RelationshipsPage:refresh_lover_list() + local choices = {} + for _, lover_hfid in ipairs(get_lovers(self:get_unit())) do + local lover_hf = df.historical_figure.find(lover_hfid) + if lover_hf then + local lover_unit = df.unit.find(lover_hf.unit_id) + local text = { + {text=get_name(lover_hf)}, + {gap=1, text=lover_unit and '' or '(off-site)', pen=COLOR_YELLOW}, + } + table.insert(choices, { + text=text, + data={hfid=lover_hfid}, + }) + end + end + local list = self.subviews.lovers + local selected = list:getSelected() + list:setChoices(choices) + list:setSelected(selected) +end + +function RelationshipsPage:render(dc) + if self.dirty > 0 then + self:updateLayout() + self.dirty = self.dirty - 1 + if self.dirty <= 0 then + self:refresh_lover_list() + end + end + RelationshipsPage.super.render(self, dc) + self.cache = {} +end + +function RelationshipsPage:get_name() + return get_name(self:get_unit()) +end + +function RelationshipsPage:get_spouse_unit() + return get_spouse_unit(self:get_unit()) +end + +---------------------- +-- PregnancyPage +-- + +PregnancyPage = defclass(PregnancyPage, widgets.Panel) -function Pregnancy:init() +function PregnancyPage:init() self.cache = {} self.mother_id, self.father_id = -1, -1 self.dirty = 0 @@ -152,17 +449,28 @@ function Pregnancy:init() frame={t=1, l=0}, text={{text=self:callback('get_pregnancy_desc')}}, }, - widgets.Label{ + widgets.HotkeyLabel{ frame={t=3, l=0}, + label="Choose selected unit to be the mother", + key='CUSTOM_SHIFT_M', + auto_width=true, + on_activate=self:callback('set_mother'), + enabled=function() + local unit = dfhack.gui.getSelectedUnit(true) + return unit and unit.id ~= self.mother_id and is_viable_parent(unit, df.pronoun_type.she) + end, + }, + widgets.Label{ + frame={t=5, l=0}, text={{text='Spouse:', pen=function() return can_have_spouse(self:get_mother()) and COLOR_WHITE or COLOR_GRAY end}}, }, widgets.Label{ - frame={t=3, l=8}, + frame={t=5, l=8}, text={{text='None', pen=function() return can_have_spouse(self:get_mother()) and COLOR_WHITE or COLOR_GRAY end}}, visible=function() return not self:get_spouse_unit('mother') and not self:get_spouse_hf('mother') end, }, widgets.Label{ - frame={t=3, l=8}, + frame={t=5, l=8}, text={{text=self:callback('get_spouse_name', 'mother')}}, text_pen=COLOR_BLUE, auto_width=true, @@ -170,7 +478,7 @@ function Pregnancy:init() visible=self:callback('get_spouse_unit', 'mother'), }, widgets.Label{ - frame={t=3, l=8}, + frame={t=5, l=8}, text={ {text=self:callback('get_spouse_hf_name', 'mother'), pen=COLOR_BLUE}, {gap=1, text='(off-site)', pen=COLOR_YELLOW}, @@ -179,71 +487,26 @@ function Pregnancy:init() visible=function() return not self:get_spouse_unit('mother') and self:get_spouse_hf('mother') end, }, widgets.HotkeyLabel{ - frame={t=4, l=2}, + frame={t=6, l=2}, label="Set mother's spouse as the father", key='CUSTOM_F', auto_width=true, on_activate=function() self:set_father(self:get_spouse_unit('mother')) end, enabled=function() local spouse = self:get_spouse_unit('mother') - return spouse and spouse.id ~= self.father_id and is_viable_partner(spouse, df.pronoun_type.he) - end, - }, - widgets.HotkeyLabel{ - frame={t=5, l=2}, - label="Dissolve spouse relationship", - key='CUSTOM_X', - auto_width=true, - on_activate=function() - clear_spouse(self:get_mother()) - self.dirty = 3 - end, - visible=function() - local mother = self:get_mother() - return mother and mother.relationship_ids.Spouse ~= -1 - end, - }, - widgets.HotkeyLabel{ - frame={t=5, l=2}, - label="Set selected father as spouse", - key='CUSTOM_X', - auto_width=true, - on_activate=function() - set_spouse(self:get_mother(), self:get_father()) - self.dirty = 3 - end, - visible=function() - local mother = self:get_mother() - return not mother or mother.relationship_ids.Spouse == -1 - end, - enabled=function() - local mother = self:get_mother() - local father = self:get_father() - return mother and mother.relationship_ids.Spouse == -1 and can_have_spouse(mother) and - father and father.relationship_ids.Spouse == -1 - end, - }, - widgets.HotkeyLabel{ - frame={t=7, l=0}, - label="Choose selected unit to be the mother", - key='CUSTOM_SHIFT_M', - auto_width=true, - on_activate=self:callback('set_mother'), - enabled=function() - local unit = dfhack.gui.getSelectedUnit(true) - return unit and unit.id ~= self.mother_id and is_viable_partner(unit, df.pronoun_type.she) + return spouse and spouse.id ~= self.father_id and is_viable_parent(spouse, df.pronoun_type.he) end, }, }, }, widgets.Divider{ - frame={t=9, h=1}, + frame={t=8, h=1}, frame_style=gui.FRAME_THIN, frame_style_l=false, frame_style_r=false, }, widgets.Panel{ - frame={t=11}, + frame={t=10}, subviews={ widgets.Label{ frame={t=0, l=0}, @@ -271,17 +534,28 @@ function Pregnancy:init() on_click=function() zoom_to(self:get_father()) end, visible=self:callback('get_father'), }, - widgets.Label{ + widgets.HotkeyLabel{ frame={t=2, l=0}, + label="Choose selected unit to be the father", + key='CUSTOM_SHIFT_F', + auto_width=true, + on_activate=self:callback('set_father'), + enabled=function() + local unit = dfhack.gui.getSelectedUnit(true) + return unit and unit.id ~= self.father_id and is_viable_parent(unit, df.pronoun_type.he) + end, + }, + widgets.Label{ + frame={t=4, l=0}, text={{text='Spouse:', pen=function() return can_have_spouse(self:get_father()) and COLOR_WHITE or COLOR_GRAY end}}, }, widgets.Label{ - frame={t=2, l=8}, + frame={t=4, l=8}, text={{text='None', pen=function() return can_have_spouse(self:get_father()) and COLOR_WHITE or COLOR_GRAY end}}, visible=function() return not self:get_spouse_unit('father') and not self:get_spouse_hf('father') end, }, widgets.Label{ - frame={t=2, l=8}, + frame={t=4, l=8}, text={{text=self:callback('get_spouse_name', 'father')}}, text_pen=function() local spouse = self:get_spouse_unit('father') @@ -295,7 +569,7 @@ function Pregnancy:init() visible=self:callback('get_spouse_unit', 'father'), }, widgets.Label{ - frame={t=2, l=8}, + frame={t=4, l=8}, text={ {text=self:callback('get_spouse_hf_name', 'father'), pen=COLOR_CYAN}, {gap=1, text='(off-site)', pen=COLOR_YELLOW}, @@ -304,71 +578,26 @@ function Pregnancy:init() visible=function() return not self:get_spouse_unit('father') and self:get_spouse_hf('father') end, }, widgets.HotkeyLabel{ - frame={t=3, l=2}, + frame={t=5, l=2}, label="Set father's spouse as the mother", key='CUSTOM_M', auto_width=true, on_activate=function() self:set_mother(self:get_spouse_unit('father')) end, enabled=function() local spouse = self:get_spouse_unit('father') - return spouse and spouse.id ~= self.mother_id and is_viable_partner(spouse, df.pronoun_type.she) - end, - }, - widgets.HotkeyLabel{ - frame={t=4, l=2}, - label="Dissolve spouse relationship", - key='CUSTOM_SHIFT_X', - auto_width=true, - on_activate=function() - clear_spouse(self:get_father()) - self.dirty = 3 - end, - visible=function() - local father = self:get_father() - return father and father.relationship_ids.Spouse ~= -1 - end, - }, - widgets.HotkeyLabel{ - frame={t=4, l=2}, - label="Set selected mother as spouse", - key='CUSTOM_SHIFT_X', - auto_width=true, - on_activate=function() - set_spouse(self:get_mother(), self:get_father()) - self.dirty = 3 - end, - visible=function() - local father = self:get_father() - return not father or father.relationship_ids.Spouse == -1 - end, - enabled=function() - local mother = self:get_mother() - local father = self:get_father() - return mother and mother.relationship_ids.Spouse == -1 and can_have_spouse(mother) and - father and father.relationship_ids.Spouse == -1 - end, - }, - widgets.HotkeyLabel{ - frame={t=6, l=0}, - label="Choose selected unit to be the father", - key='CUSTOM_SHIFT_F', - auto_width=true, - on_activate=self:callback('set_father'), - enabled=function() - local unit = dfhack.gui.getSelectedUnit(true) - return unit and unit.id ~= self.father_id and is_viable_partner(unit, df.pronoun_type.he) + return spouse and spouse.id ~= self.mother_id and is_viable_parent(spouse, df.pronoun_type.she) end, }, }, }, widgets.Divider{ - frame={t=19, h=1}, + frame={t=17, h=1}, frame_style=gui.FRAME_THIN, frame_style_l=false, frame_style_r=false, }, widgets.Panel{ - frame={t=21}, + frame={t=19}, subviews={ widgets.CycleHotkeyLabel{ view_id='term', @@ -407,42 +636,47 @@ function Pregnancy:init() }, }, } +end +function PregnancyPage:on_show() local unit = dfhack.gui.getSelectedUnit(true) - self:set_mother(unit) - self:set_father(unit) + if self.mother_id == -1 then + self:set_mother(unit) + end + if self.father_id == -1 then + self:set_father(unit) + end end -function Pregnancy:get_mother() +function PregnancyPage:get_mother() self.cache.mother = self.cache.mother or df.unit.find(self.mother_id) return self.cache.mother end -function Pregnancy:get_father() +function PregnancyPage:get_father() self.cache.father = self.cache.father or df.unit.find(self.father_id) return self.cache.father end -function Pregnancy:render(dc) +function PregnancyPage:render(dc) if self.dirty > 0 then -- needs multiple iterations of updateLayout because of multiple -- layers of indirection in the text generation self:updateLayout() self.dirty = self.dirty - 1 end - Pregnancy.super.render(self, dc) + PregnancyPage.super.render(self, dc) self.cache = {} end -function Pregnancy:get_name(who) - local unit = self['get_'..who](self) - return unit and dfhack.units.getReadableName(unit) or '' +function PregnancyPage:get_name(who) + return get_name(self['get_'..who](self)) end local TICKS_PER_DAY = 1200 local TICKS_PER_MONTH = 28 * TICKS_PER_DAY -function Pregnancy:get_pregnancy_desc() +function PregnancyPage:get_pregnancy_desc() local mother = self:get_mother() if not mother or not mother.pregnancy_genes or mother.pregnancy_timer <= 0 then return 'Not currently pregnant' @@ -458,46 +692,25 @@ function Pregnancy:get_pregnancy_desc() return ('Currently pregnant: coming to term %s'):format(term_str) end -function Pregnancy:get_spouse_unit(who) - local unit = self['get_'..who](self) - if not unit then return end - return df.unit.find(unit.relationship_ids.Spouse) +function PregnancyPage:get_spouse_unit(who) + return get_spouse_unit(self['get_'..who](self)) end -function Pregnancy:get_spouse_hf(who) - local unit = self['get_'..who](self) - if not unit or unit.relationship_ids.Spouse == -1 then - return - end - local spouse = df.unit.find(unit.relationship_ids.Spouse) - if spouse then - return df.historical_figure.find(spouse.hist_figure_id) - end - - local hf = df.historical_figure.find(unit.hist_figure_id) - if not hf then return end - - for _, link in ipairs(hf.histfig_links) do - if link._type == df.histfig_hf_link_spousest then - -- may be nil due to hf culling, but then we just treat it as not having a spouse - return df.historical_figure.find(link.target_hf) - end - end +function PregnancyPage:get_spouse_hf(who) + return get_spouse_hf(self['get_'..who](self)) end -function Pregnancy:get_spouse_name(who) - local spouse = self:get_spouse_unit(who) - return spouse and dfhack.units.getReadableName(spouse) or '' +function PregnancyPage:get_spouse_name(who) + return get_name(self:get_spouse_unit(who)) end -function Pregnancy:get_spouse_hf_name(who) - local spouse_hf = self:get_spouse_hf(who) - return spouse_hf and dfhack.units.getReadableName(spouse_hf) or '' +function PregnancyPage:get_spouse_hf_name(who) + return get_name(self:get_spouse_hf(who)) end -function Pregnancy:set_mother(unit) +function PregnancyPage:set_mother(unit) unit = unit or dfhack.gui.getSelectedUnit(true) - if not is_viable_partner(unit, df.pronoun_type.she) then return end + if not is_viable_parent(unit, df.pronoun_type.she) then return end local father = self:get_father() local function do_set_mother() if self.father_id ~= -1 then @@ -522,9 +735,9 @@ function Pregnancy:set_mother(unit) end end -function Pregnancy:set_father(unit) +function PregnancyPage:set_father(unit) unit = unit or dfhack.gui.getSelectedUnit(true) - if not is_viable_partner(unit, df.pronoun_type.he) then return end + if not is_viable_parent(unit, df.pronoun_type.he) then return end local mother = self:get_mother() local function do_set_father() if self.mother_id ~= -1 then @@ -556,7 +769,7 @@ local function get_term_ticks(months) return ticks end -function Pregnancy:commit() +function PregnancyPage:commit() local mother = self:get_mother() local father = self:get_father() or mother @@ -584,20 +797,64 @@ function Pregnancy:commit() end ---------------------- --- PregnancyScreen +-- FamilyAffairs +-- + +FamilyAffairs = defclass(FamilyAffairs, widgets.Window) +FamilyAffairs.ATTRS { + frame_title='Family manager', + frame={w=50, h=30, r=2, t=18}, + frame_inset={t=1, l=1, r=1}, + resizable=true, +} + +function FamilyAffairs:init() + local function on_show() + local _, page = self.subviews.pages:getSelected() + page:on_show() + end + + self:addviews{ + widgets.TabBar{ + frame={t=0, l=0}, + labels={ + 'Relationships', + 'Pregnancy', + }, + on_select=function(idx) + self.subviews.pages:setSelected(idx) + on_show() + end, + get_cur_page=function() return self.subviews.pages:getSelected() end, + }, + widgets.Pages{ + view_id='pages', + frame={t=3, l=0, b=0, r=0}, + subviews={ + RelationshipsPage{}, + PregnancyPage{}, + }, + }, + } + + on_show() +end + +---------------------- +-- FamilyAffairsScreen -- -PregnancyScreen = defclass(PregnancyScreen, gui.ZScreen) -PregnancyScreen.ATTRS { +FamilyAffairsScreen = defclass(FamilyAffairsScreen, gui.ZScreen) +FamilyAffairsScreen.ATTRS { focus_path='pregnancy', } -function PregnancyScreen:init() - self:addviews{Pregnancy{}} +function FamilyAffairsScreen:init() + self:addviews{FamilyAffairs{}} end -function PregnancyScreen:onDismiss() +function FamilyAffairsScreen:onDismiss() view = nil end -view = view and view:raise() or PregnancyScreen{}:show() +view = view and view:raise() or FamilyAffairsScreen{}:show() From 15abc3ad3d2eb542a959a82b5d5386c0f7f372e7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 24 Aug 2024 18:26:58 -0700 Subject: [PATCH 011/811] fix typo --- docs/brainwash.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/brainwash.rst b/docs/brainwash.rst index cbd952d22f..9c1eef4ed1 100644 --- a/docs/brainwash.rst +++ b/docs/brainwash.rst @@ -12,6 +12,7 @@ Usage ----- :: + brainwash Examples From 41f19623cf7fe9cbb0213de026c310c408af1ca1 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sat, 17 Aug 2024 00:11:36 -0500 Subject: [PATCH 012/811] docs spelling fixes --- changelog.txt | 16 ++++++++-------- docs/agitation-rebalance.rst | 6 +++--- docs/caravan.rst | 4 ++-- docs/confirm.rst | 2 +- docs/devel/dump-tooltip-ids.rst | 2 +- docs/devel/input-monitor.rst | 2 +- docs/devel/spawn-unit-helper.rst | 4 ++-- docs/fix/dead-units.rst | 2 +- docs/fix/empty-wheelbarrows.rst | 4 ++-- docs/fix/sleepers.rst | 2 +- docs/gui/journal.rst | 2 +- docs/gui/launcher.rst | 2 +- docs/gui/settings-manager.rst | 2 +- docs/gui/teleport.rst | 2 +- docs/gui/tiletypes.rst | 2 +- docs/list-waves.rst | 2 +- 16 files changed, 28 insertions(+), 28 deletions(-) diff --git a/changelog.txt b/changelog.txt index 1c305263a5..5d98150572 100644 --- a/changelog.txt +++ b/changelog.txt @@ -137,7 +137,7 @@ Template for new versions: - `caravan`: remember filter settings for pedestal item assignment dialog - `quickfort`: new ``delete`` command for deleting player-owned blueprints (library and mod-added blueprints cannot be deleted) - `quickfort`: support enabling `logistics` features for autoforbid and autoclaim on stockpiles -- `gui/quickfort`: allow farm plots, dirt roads, and paved roads to be designated around partial obstructions without callling it an error, matching vanilla behavior +- `gui/quickfort`: allow farm plots, dirt roads, and paved roads to be designated around partial obstructions without calling it an error, matching vanilla behavior - `gui/launcher`: refresh default tag filter when mortal mode is toggled in `gui/control-panel` so changes to which tools autocomplete take effect immediately - `gui/civ-alert`: you can now register multiple burrows as civilian alert safe spaces - `exterminate`: add ``all`` target for convenient scorched earth tactics @@ -209,7 +209,7 @@ Template for new versions: ## Fixes - `open-legends`: don't interfere with the dragging of vanilla list scrollbars - `gui/create-item`: properly restrict bags to bag materials by default -- `gui/create-item`: allow gloves and shoees to be made out of textiles by default +- `gui/create-item`: allow gloves and shoes to be made out of textiles by default - `exterminate`: don't classify dangerous non-invader units as friendly (e.g. snatchers) ## Misc Improvements @@ -257,7 +257,7 @@ Template for new versions: # 50.12-r2 ## New Tools -- `agitation-rebalance`: alter mechanics of irriation-related attacks so they are less constant and are more responsive to recent player bahavior +- `agitation-rebalance`: alter mechanics of irritation-related attacks so they are less constant and are more responsive to recent player behavior - `fix/ownership`: fix instances of multiple citizens claiming the same items, resulting in "Store owned item" job loops - `fix/stuck-worship`: fix prayer so units don't get stuck in uninterruptible "Worship!" states - `instruments`: provides information on how to craft the instruments used by the player civilization @@ -306,7 +306,7 @@ Template for new versions: - `gui/notify`: display important notifications that vanilla doesn't support yet and provide quick zoom links to notification targets. - `list-waves`: (reinstated) show migration wave information - `make-legendary`: (reinstated) make a dwarf legendary in specified skills -- `combat-harden`: (reinstated) set a dwarf's resistence to being affected by visible corpses +- `combat-harden`: (reinstated) set a dwarf's resistance to being affected by visible corpses - `add-thought`: (reinstated) add custom thoughts to a dwarf - `devel/input-monitor`: interactive UI for debugging input issues @@ -565,7 +565,7 @@ Template for new versions: - `workorder`: reduce existing orders for automatic shearing and milking jobs when animals are no longer available - `gui/quickfort`: adapt "cursor lock" to mouse controls so it's easier to see the full preview for multi-level blueprints before you apply them - `gui/quickfort`: only display post-blueprint messages once when repeating the blueprint up or down z-levels -- `combine`: reduce max different stacks in containers to 30 to prevent contaners from getting overfull +- `combine`: reduce max different stacks in containers to 30 to prevent containers from getting overfull ## Removed - `gui/automelt`: replaced by an overlay panel that appears when you click on a stockpile @@ -597,7 +597,7 @@ Template for new versions: ## Misc Improvements - `gui/quickfort`: blueprints that designate items for dumping/forbidding/etc. no longer show an error highlight for tiles that have no items on them - `gui/quickfort`: place (stockpile layout) mode is now supported. note that detailed stockpile configurations were part of query mode and are not yet supported -- `gui/quickfort`: you can now generate manager orders for items required to complete bluerpints +- `gui/quickfort`: you can now generate manager orders for items required to complete blueprints - `gui/create-item`: ask for number of items to spawn by default - `light-aquifers-only`: now available as a fort Autostart option in `gui/control-panel`. note that it will only appear if "armok" tools are configured to be shown on the Preferences tab. - `gui/gm-editor`: when passing the ``--freeze`` option, further ensure that the game is frozen by halting all rendering (other than for DFHack tool windows) @@ -713,7 +713,7 @@ Template for new versions: - `troubleshoot-item`: reports on the contents of containers with counts for each contained item type - `devel/visualize-structure`: now automatically inspects the contents of most pointer fields, rather than inspecting the pointers themselves - `devel/query`: will now search for jobs at the map coordinate highlighted, if no explicit job is highlighted and there is a map tile highlighted -- `caravan`: add trade screen overlay that assists with seleting groups of items and collapsing groups in the UI +- `caravan`: add trade screen overlay that assists with selecting groups of items and collapsing groups in the UI - `gui/gm-editor`: will now inspect a selected building itself if the building has no current jobs ## Removed @@ -779,7 +779,7 @@ Template for new versions: - `gui/quickfort`: don't close the window when applying a blueprint so players can apply the same blueprint multiple times more easily - `locate-ore`: now only searches revealed tiles by default - `modtools/spawn-liquid`: sets tile temperature to stable levels when spawning water or magma --@ `prioritize`: pushing minecarts is now included in the default priortization list +-@ `prioritize`: pushing minecarts is now included in the default prioritization list - `prioritize`: now automatically starts boosting the default list of job types when enabled - `unforbid`: avoids unforbidding unreachable and underwater items by default - `gui/create-item`: added whole corpse spawning alongside corpsepieces. (under "corpse") diff --git a/docs/agitation-rebalance.rst b/docs/agitation-rebalance.rst index 790af212b4..435d06225e 100644 --- a/docs/agitation-rebalance.rst +++ b/docs/agitation-rebalance.rst @@ -130,7 +130,7 @@ Irritation counters are saved with the cavern layer in the world region, which extends beyond the boundaries of your current fort. If you retire a fort and start another one nearby, the caverns will retain any irritation added by the first fort. This means that new forts may start with already-irritated caverns -and meet with immediate resistence. +and meet with immediate resistance. The settings ~~~~~~~~~~~~ @@ -182,7 +182,7 @@ When enabled, this mod makes the following changes: When agitated wildlife enters the map on the surface, the surface irritation counter is set to the value of ``Wilderness irritation minimum``, ensuring -that the *next* group of widlife that enters the map will *not* be agitated. +that the *next* group of wildlife that enters the map will *not* be agitated. This means that the incursions act more like a warning shot than an open floodgate. You will not be attacked again unless you continue your activities on the surface that raise the chance of a subsequent attack. @@ -211,7 +211,7 @@ attacks can still be controlled independently of this mod. Finally, if you have walled yourself off from the danger in the caverns, yet you continue to irritate nature down there, this mod will ensure that the number of -active cavern invaders, cumulative across all cavern levels, never exeeds the +active cavern invaders, cumulative across all cavern levels, never exceeds the value set for ``Cavern dweller maximum attackers``. This prevents excessive FPS loss during gameplay and keeps the number of creatures milling around outside your gates (or hidden in the shadows) to a reasonable number. diff --git a/docs/caravan.rst b/docs/caravan.rst index 1e365cd159..3208514633 100644 --- a/docs/caravan.rst +++ b/docs/caravan.rst @@ -74,7 +74,7 @@ selected, then the range of items will be selected. If any current merchants have ethical concerns, the list of goods that you can bring to the depot is automatically filtered (by default) to only show -ethically acceptible items. Be aware that, again, by default, if you have items +ethically acceptable items. Be aware that, again, by default, if you have items in bins, and there are unethical items mixed into the bins, then the bins will still be brought to the depot so you can trade the ethical items within those bins. Please use the DFHack enhanced trade screen for the actual barter to @@ -96,7 +96,7 @@ Trade screen **caravan.trade** -This overlay enables some convenent gestures and keyboard shortcuts for working +This overlay enables some convenient gestures and keyboard shortcuts for working with bins: - ``Shift-Click checkbox``: Select all items inside a bin without selecting the diff --git a/docs/confirm.rst b/docs/confirm.rst index ab7dff04d9..90408f0eb4 100644 --- a/docs/confirm.rst +++ b/docs/confirm.rst @@ -5,7 +5,7 @@ confirm :summary: Adds confirmation dialogs for destructive actions. :tags: fort interface -In the base game, it is frightenly easy to destroy hours of work with a single +In the base game, it is frighteningly easy to destroy hours of work with a single misclick. Now you can avoid the consequences of accidentally disbanding a squad (for example), or deleting a hauling route. diff --git a/docs/devel/dump-tooltip-ids.rst b/docs/devel/dump-tooltip-ids.rst index beb6785221..5f066b94b0 100644 --- a/docs/devel/dump-tooltip-ids.rst +++ b/docs/devel/dump-tooltip-ids.rst @@ -19,7 +19,7 @@ as item 500, but we detect that caption at position 501 in ``main_interface.hove produced by the script will include the above element at position 501 instead of 500. Before running this script, the size of ``main_interface.hover_instruction`` must be aligned properly with the -loaded verison of DF so the array of strings can be read. +loaded version of DF so the array of strings can be read. Usage ----- diff --git a/docs/devel/input-monitor.rst b/docs/devel/input-monitor.rst index 8d271084c2..e20947cc27 100644 --- a/docs/devel/input-monitor.rst +++ b/docs/devel/input-monitor.rst @@ -11,7 +11,7 @@ and mouse device. The labels for Shift, Ctrl, and Alt light up when those modifier keys are being held down. -Similar lables for left, middle, and right mouse buttons light up when any of +Similar labels for left, middle, and right mouse buttons light up when any of those buttons are being held down. The input stream panel shows the keybindings that are being triggered. You can diff --git a/docs/devel/spawn-unit-helper.rst b/docs/devel/spawn-unit-helper.rst index c45dcf8e1e..fdf545d741 100644 --- a/docs/devel/spawn-unit-helper.rst +++ b/docs/devel/spawn-unit-helper.rst @@ -13,8 +13,8 @@ Usage 1. Enter the :kbd:`k` menu and change mode using ``rb_eval df.gametype = :DWARF_ARENA`` -2. Spawn creatures with the normal arena mode UI (:kbd:`c` ingame) -3. Revert to forgress mode using +2. Spawn creatures with the normal arena mode UI (:kbd:`c` in-game) +3. Revert to fortress mode using ``rb_eval df.gametype = #{df.gametype.inspect}`` 4. To convert spawned creatures to livestock, select each one with the :kbd:`v` menu, and enter ``rb_eval df.unit_find.civ_id = df.ui.civ_id`` diff --git a/docs/fix/dead-units.rst b/docs/fix/dead-units.rst index 49156be5f7..4f194262ee 100644 --- a/docs/fix/dead-units.rst +++ b/docs/fix/dead-units.rst @@ -32,4 +32,4 @@ Options ``--burrow`` Scrub dead units from burrow membership lists. ``-q``, ``--quiet`` - Surpress console output (final status update is still printed if at least one item was affected). + Suppress console output (final status update is still printed if at least one item was affected). diff --git a/docs/fix/empty-wheelbarrows.rst b/docs/fix/empty-wheelbarrows.rst index eb6d155104..9fcbe15c70 100644 --- a/docs/fix/empty-wheelbarrows.rst +++ b/docs/fix/empty-wheelbarrows.rst @@ -26,12 +26,12 @@ Examples ``fix/empty-wheelbarrows --dry-run`` Lists all wheelbarrows that would be emptied and their contents without performing the action. ``fix/empty-wheelbarrows --quiet`` - Does the action while surpressing output to console. + Does the action while suppressing output to console. Options ------- ``-q``, ``--quiet`` - Surpress console output (final status update is still printed if at least one item was affected). + Suppress console output (final status update is still printed if at least one item was affected). ``-d``, ``--dry-run`` Dry run, don't commit changes. diff --git a/docs/fix/sleepers.rst b/docs/fix/sleepers.rst index 1b3a0bc6ae..bb316ad535 100644 --- a/docs/fix/sleepers.rst +++ b/docs/fix/sleepers.rst @@ -7,7 +7,7 @@ fix/sleepers Fixes :bug:`6798`. This bug is characterized by sleeping units who refuse to awaken in adventure mode regardless of talking to them, hitting them, or waiting -so long you die of thirst. If you come accross one or more bugged sleepers in +so long you die of thirst. If you come across one or more bugged sleepers in adventure mode, simply run the script and all nearby sleepers will be cured. Usage diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index ba3619d2e1..d6004ec95d 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -14,7 +14,7 @@ and both short-term and long-term plans. This is particularly useful when you need to take a longer break from the game. Having detailed notes makes it much easier to resume your game after -a few weekds or months, without losing track of your progress and objectives. +a few weeks or months, without losing track of your progress and objectives. Supported Features ------------------ diff --git a/docs/gui/launcher.rst b/docs/gui/launcher.rst index fdfe2cb729..f5e39579bc 100644 --- a/docs/gui/launcher.rst +++ b/docs/gui/launcher.rst @@ -118,7 +118,7 @@ Default tag filters By default, commands intended for developers and modders are filtered out of the autocomplete list. This includes any tools tagged with ``unavailable``. If you have "mortal mode" enabled in the `gui/control-panel` preferences, any tools -with the ``armok`` tag are filterd out as well. +with the ``armok`` tag are filtered out as well. You can toggle this default filtering by hitting :kbd:`Ctrl`:kbd:`D` to switch into "Dev mode" at any time. You can also adjust your command filters in the diff --git a/docs/gui/settings-manager.rst b/docs/gui/settings-manager.rst index 683ccb8917..1c57fe1a67 100644 --- a/docs/gui/settings-manager.rst +++ b/docs/gui/settings-manager.rst @@ -45,7 +45,7 @@ automatically restored when you embark on a new fort. This will toggle the relevant command in `gui/control-panel` on the Automation -> Autostart page. There is a similar panel on the Labor -> Work Details page that allows for -saving and restoring of work detail definitons. Be aware that work detail +saving and restoring of work detail definitions. Be aware that work detail assignments to units cannot be saved, so you have to assign the work details to individual units after you restore the definitions. Another caveat is that DF doesn't evaluate work detail definitions until a change (any change) is made on diff --git a/docs/gui/teleport.rst b/docs/gui/teleport.rst index 6222af4f8d..513f26bbb0 100644 --- a/docs/gui/teleport.rst +++ b/docs/gui/teleport.rst @@ -14,7 +14,7 @@ pre-selected for teleport. Note that you *can* select enemies that are lying in ambush and are not visible on the map yet, so you if you select an area and see a marker that indicates that a unit is selected, but you don't see the unit itself, this is likely what -it is. You can stil teleport these units while they are hidden. +it is. You can still teleport these units while they are hidden. Usage ----- diff --git a/docs/gui/tiletypes.rst b/docs/gui/tiletypes.rst index 578238c8e2..62b2fdbe15 100644 --- a/docs/gui/tiletypes.rst +++ b/docs/gui/tiletypes.rst @@ -79,7 +79,7 @@ smooth. Note that when creating walls, they will inherit the smoothness property of whatever was there before unless you specifically set the Special selector to ``NORMAL`` (for rough walls) or ``SMOOTH`` (for smooth walls). -Extended special properties are avaialable via the gear button. +Extended special properties are available via the gear button. Variant ~~~~~~~ diff --git a/docs/list-waves.rst b/docs/list-waves.rst index 52eb022d8e..c86e63a391 100644 --- a/docs/list-waves.rst +++ b/docs/list-waves.rst @@ -20,7 +20,7 @@ Usage list-waves [ ...] [] -You can show only information about specific waves by specifing the wave +You can show only information about specific waves by specifying the wave numbers on the commandline. Otherwise, all waves are shown. The first migration wave that normally arrives in a fort's second season is wave number 1. The founding dwarves arrive in wave 0. From a9d62208b205d4dd8082cfbe491352fcd89c1cd0 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sat, 17 Aug 2024 00:15:28 -0500 Subject: [PATCH 013/811] docs: syndrome-trigger is under modtools --- docs/modtools/syndrome-trigger.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/modtools/syndrome-trigger.rst b/docs/modtools/syndrome-trigger.rst index 7e915fda1a..dccf02e610 100644 --- a/docs/modtools/syndrome-trigger.rst +++ b/docs/modtools/syndrome-trigger.rst @@ -13,9 +13,9 @@ Usage :: - modutils/syndrome-trigger --clear - modutils/syndrome-trigger --syndrome --command [ ] - modutils/syndrome-trigger --synclass --command [ ] + modtools/syndrome-trigger --clear + modtools/syndrome-trigger --syndrome --command [ ] + modtools/syndrome-trigger --synclass --command [ ] Options ------- @@ -41,4 +41,4 @@ Examples :: - modutils/syndrome-trigger --synclass VAMPCURSE --command [ modtools/spawn-flow -flowType Dragonfire -location [ \\LOCATION ] ] + modtools/syndrome-trigger --synclass VAMPCURSE --command [ modtools/spawn-flow -flowType Dragonfire -location [ \\LOCATION ] ] From a26f3b0ca3a4b3c037c8ae5ab0e79682e9ead09a Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sat, 17 Aug 2024 00:16:53 -0500 Subject: [PATCH 014/811] docs: fix old DF-spelling in modtools/set-need.rst Per the comment in the code, this looks like an old DF-originated misspelling that has been fixed for a while. --- docs/modtools/set-need.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/modtools/set-need.rst b/docs/modtools/set-need.rst index e2199553c3..da794939bc 100644 --- a/docs/modtools/set-need.rst +++ b/docs/modtools/set-need.rst @@ -28,9 +28,9 @@ Valid need targets: :need : ID of the need to target. For example 0 or DrinkAlcohol. - If the need is PrayOrMedidate, a -deity argument is also required. + If the need is PrayOrMeditate, a -deity argument is also required. :deity : - Required when using PrayOrMedidate needs. This value should be the historical figure ID of the deity in question. + Required when using PrayOrMeditate needs. This value should be the historical figure ID of the deity in question. :all: All of the target's needs will be affected. From 2ef861b0c1869fa996cef2116d349024bd5c37ef Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 25 Aug 2024 12:15:17 -0700 Subject: [PATCH 015/811] final first draft of relationships tab also fix how spouses are looked up --- gui/family-affairs.lua | 155 +++++++++++++++++++---------------------- 1 file changed, 70 insertions(+), 85 deletions(-) diff --git a/gui/family-affairs.lua b/gui/family-affairs.lua index 6de9cbd108..63c2800f24 100644 --- a/gui/family-affairs.lua +++ b/gui/family-affairs.lua @@ -1,3 +1,4 @@ +local argparse = require('argparse') local dlg = require('gui.dialogs') local gui = require('gui') local widgets = require('gui.widgets') @@ -8,7 +9,8 @@ local widgets = require('gui.widgets') local function zoom_to(unit) if not unit then return end - dfhack.gui.revealInDwarfmodeMap(xyz2pos(dfhack.units.getPosition(unit)), true, true) + local pos = xyz2pos(dfhack.units.getPosition(unit)) + dfhack.gui.revealInDwarfmodeMap(pos, true, true) end local function is_viable_parent(unit, required_pronoun) @@ -21,7 +23,7 @@ local function can_have_spouse(unit) return caste_flags.CAN_LEARN and not caste_flags.SLOW_LEARNER end --- clears other_hf's link to hf (assumes there's only one reverse link) +-- clears other_hf's link to hf (will clear only one reverse link if there are multiple) local function remove_hf_link(link_type, hfid, other_hfid) local other_hf = df.historical_figure.find(other_hfid) if not other_hf then return end @@ -86,23 +88,10 @@ local function get_name(unit_or_hf) return unit_or_hf and dfhack.units.getReadableName(unit_or_hf) or '' end -local function get_spouse_unit(unit) - if not unit then return end - return df.unit.find(unit.relationship_ids.Spouse) -end - local function get_spouse_hf(unit) - if not unit or unit.relationship_ids.Spouse == -1 then - return - end - local spouse = df.unit.find(unit.relationship_ids.Spouse) - if spouse then - return df.historical_figure.find(spouse.hist_figure_id) - end - + if not unit then return end local hf = df.historical_figure.find(unit.hist_figure_id) if not hf then return end - for _, link in ipairs(hf.histfig_links) do if link._type == df.histfig_hf_link_spousest then -- may be nil due to hf culling, but then we just treat it as not having a spouse @@ -111,15 +100,22 @@ local function get_spouse_hf(unit) end end -local function clear_spouse(unit, noconfirm) +local function get_spouse_unit(unit) + local spouse_hf = get_spouse_hf(unit) + if not spouse_hf then return end + return df.unit.find(spouse_hf.unit_id) +end + +local function clear_spouse(unit, accept_fn, noconfirm) if not unit then return end local function do_clear_spouse() + local spouse = get_spouse_unit(unit) clear_hf_links(df.histfig_hf_link_spousest, unit.hist_figure_id) - local spouse = df.unit.find(unit.relationship_ids.Spouse) if spouse then spouse.relationship_ids.Spouse = -1 end unit.relationship_ids.Spouse = -1 + accept_fn() end if noconfirm then do_clear_spouse() @@ -140,10 +136,10 @@ local function add_hf_link(link_type, hfid, other_hfid) hf.histfig_links:insert('#', link) end -local function set_spouse(unit1, unit2) +local function set_spouse(unit1, unit2, accept_fn) local function do_set_spouse() - clear_spouse(unit1, true) - clear_spouse(unit2, true) + clear_spouse(unit1, function() end, true) + clear_spouse(unit2, function() end, true) unit1.relationship_ids.Spouse = unit2.id unit2.relationship_ids.Spouse = unit1.id add_hf_link(df.histfig_hf_link_spousest, unit1.hist_figure_id, unit2.hist_figure_id) @@ -151,6 +147,7 @@ local function set_spouse(unit1, unit2) dfhack.gui.showAutoAnnouncement(df.announcement_type.MARRIAGE, xyz2pos(dfhack.units.getPosition(unit1)), ('%s and %s have married!'):format(dfhack.TranslateName(unit1.name), dfhack.TranslateName(unit2.name)), COLOR_LIGHTMAGENTA) + accept_fn() end local unit1_has_lovers = has_hf_links(df.histfig_hf_link_loverst, unit1.hist_figure_id) local unit2_has_lovers = has_hf_links(df.histfig_hf_link_loverst, unit2.hist_figure_id) @@ -199,7 +196,7 @@ function RelationshipsPage:init() }, widgets.HotkeyLabel{ frame={t=2, l=0}, - label="Inspect selected unit", + label="Switch to selected unit", key='CUSTOM_U', auto_width=true, on_activate=self:callback('set_unit'), @@ -239,14 +236,8 @@ function RelationshipsPage:init() label="Dissolve spouse relationship", key='CUSTOM_X', auto_width=true, - on_activate=function() - clear_spouse(self:get_unit()) - self.dirty = 2 - end, - visible=function() - local unit = self:get_unit() - return unit and unit.relationship_ids.Spouse ~= -1 - end, + on_activate=function() clear_spouse(self:get_unit(), function() self.dirty = 2 end) end, + visible=function() return get_spouse_hf(self:get_unit()) end, }, widgets.HotkeyLabel{ frame={t=5, l=2}, @@ -254,22 +245,18 @@ function RelationshipsPage:init() key='CUSTOM_X', auto_width=true, on_activate=function() - set_spouse(self:get_unit(), dfhack.gui.getSelectedUnit(true)) - self.dirty = 2 - end, - visible=function() - local unit = self:get_unit() - return not unit or unit.relationship_ids.Spouse == -1 + set_spouse(self:get_unit(), dfhack.gui.getSelectedUnit(true), function() self.dirty = 2 end) end, + visible=function() return not get_spouse_hf(self:get_unit()) end, enabled=function() local unit = self:get_unit() local selected = dfhack.gui.getSelectedUnit(true) - return unit and unit.relationship_ids.Spouse == -1 and can_have_spouse(unit) and + return unit and not get_spouse_hf(unit) and can_have_spouse(unit) and selected and selected.race == unit.race and selected.id ~= unit.id end, }, widgets.Panel{ - frame={t=7, b=3}, + frame={t=7, b=0}, frame_style=gui.FRAME_INTERIOR, subviews={ widgets.Label{ @@ -280,7 +267,7 @@ function RelationshipsPage:init() }, }, widgets.List{ - frame={t=2, b=2}, + frame={t=2, b=3}, view_id='lovers', on_submit=function(_, choice) local hf = df.historical_figure.find(choice.data.hfid) @@ -290,6 +277,27 @@ function RelationshipsPage:init() zoom_to(unit) end, }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + label='Add selected unit as lover', + key='CUSTOM_L', + auto_width=true, + on_activate=function() + local selected = dfhack.gui.getSelectedUnit(true) + if not selected then return end + local unit = self:get_unit() + if not unit then return end + add_hf_link(df.histfig_hf_link_loverst, unit.hist_figure_id, selected.hist_figure_id) + add_hf_link(df.histfig_hf_link_loverst, selected.hist_figure_id, unit.hist_figure_id) + self.dirty = 1 + end, + enabled=function() + local unit = self:get_unit() + local selected = dfhack.gui.getSelectedUnit(true) + return unit and selected and selected.race == unit.race and selected.id ~= unit.id and + not has_spouse_or_lover(unit, selected) + end, + }, widgets.HotkeyLabel{ frame={b=0, l=0}, label='Remove lover', @@ -311,43 +319,6 @@ function RelationshipsPage:init() }, }, }, - widgets.HotkeyLabel{ - frame={b=1, l=0}, - label='Add selected unit as spouse', - key='CUSTOM_S', - auto_width=true, - on_activate=function() - set_spouse(self:get_unit(), dfhack.gui.getSelectedUnit(true)) - self.dirty = 3 - end, - enabled=function() - local unit = self:get_unit() - local selected = dfhack.gui.getSelectedUnit(true) - return unit and selected and selected.race == unit.race and selected.id ~= unit.id and - not has_spouse_or_lover(unit, selected) - end, - }, - widgets.HotkeyLabel{ - frame={b=0, l=0}, - label='Add selected unit as lover', - key='CUSTOM_L', - auto_width=true, - on_activate=function() - local selected = dfhack.gui.getSelectedUnit(true) - if not selected then return end - local unit = self:get_unit() - if not unit then return end - add_hf_link(df.histfig_hf_link_loverst, unit.hist_figure_id, selected.hist_figure_id) - add_hf_link(df.histfig_hf_link_loverst, selected.hist_figure_id, unit.hist_figure_id) - self.dirty = 1 - end, - enabled=function() - local unit = self:get_unit() - local selected = dfhack.gui.getSelectedUnit(true) - return unit and selected and selected.race == unit.race and selected.id ~= unit.id and - not has_spouse_or_lover(unit, selected) - end, - }, } end @@ -806,11 +777,14 @@ FamilyAffairs.ATTRS { frame={w=50, h=30, r=2, t=18}, frame_inset={t=1, l=1, r=1}, resizable=true, + initial_tab=DEFAULT_NIL, } function FamilyAffairs:init() - local function on_show() - local _, page = self.subviews.pages:getSelected() + local function on_select(idx) + local pages = self.subviews.pages + pages:setSelected(idx) + local _, page = pages:getSelected() page:on_show() end @@ -821,10 +795,7 @@ function FamilyAffairs:init() 'Relationships', 'Pregnancy', }, - on_select=function(idx) - self.subviews.pages:setSelected(idx) - on_show() - end, + on_select=on_select, get_cur_page=function() return self.subviews.pages:getSelected() end, }, widgets.Pages{ @@ -837,7 +808,7 @@ function FamilyAffairs:init() }, } - on_show() + on_select(self.initial_tab == 'pregnancy' and 2 or 1) end ---------------------- @@ -847,14 +818,28 @@ end FamilyAffairsScreen = defclass(FamilyAffairsScreen, gui.ZScreen) FamilyAffairsScreen.ATTRS { focus_path='pregnancy', + initial_tab='relationships', } function FamilyAffairsScreen:init() - self:addviews{FamilyAffairs{}} + self:addviews{FamilyAffairs{initial_tab=self.initial_tab}} end function FamilyAffairsScreen:onDismiss() view = nil end -view = view and view:raise() or FamilyAffairsScreen{}:show() +local help, initial_tab = false, 'relationships' + +local positionals = argparse.processArgsGetopt({...}, { + {'h', 'help', handler=function() help = true end}, + {nil, 'pregnancy', handler=function() initial_tab = 'pregnancy' end}, +}) + +if positionals[1] == 'help' then help = true end +if help then + print(dfhack.script_help()) + return +end + +view = view and view:raise() or FamilyAffairsScreen{initial_tab=initial_tab}:show() From 7987e02887b2c0bab7746dd398811125663860db Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 25 Aug 2024 12:19:09 -0700 Subject: [PATCH 016/811] update changelog --- changelog.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 1c305263a5..bd91a353f6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,7 +29,7 @@ Template for new versions: ## New Tools - `embark-anyone`: allows you to embark as any civilisation, including dead, and non-dwarven ones - `idle-crafting`: Allow dwarfs to automatically satisfy their need to craft objects. -- `gui/pregnancy`: view and generate pregnancies or arrange marriages with specified partners +- `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationsips ## New Features - `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them @@ -46,7 +46,6 @@ Template for new versions: - `gui/sitemap`: show whether a unit is caged ## Removed -- `gui/family-affairs`: merged into `gui/pregnancy` # 50.13-r4 From 1b925cd9652f9992220018fe6cbb88027a3913f7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 25 Aug 2024 19:27:44 -0700 Subject: [PATCH 017/811] require a map to be loaded to run gui/family-affairs --- gui/family-affairs.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/gui/family-affairs.lua b/gui/family-affairs.lua index 63c2800f24..31c9a322e1 100644 --- a/gui/family-affairs.lua +++ b/gui/family-affairs.lua @@ -829,6 +829,10 @@ function FamilyAffairsScreen:onDismiss() view = nil end +if not dfhack.isMapLoaded() then + qerror('requires a map to be loaded') +end + local help, initial_tab = false, 'relationships' local positionals = argparse.processArgsGetopt({...}, { From 20a01478de34f2d6a0fcad6f0557991a100a6a83 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 25 Aug 2024 19:40:58 -0700 Subject: [PATCH 018/811] require spouses to be of opposite sex --- gui/family-affairs.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gui/family-affairs.lua b/gui/family-affairs.lua index 31c9a322e1..c0a64447b5 100644 --- a/gui/family-affairs.lua +++ b/gui/family-affairs.lua @@ -252,7 +252,9 @@ function RelationshipsPage:init() local unit = self:get_unit() local selected = dfhack.gui.getSelectedUnit(true) return unit and not get_spouse_hf(unit) and can_have_spouse(unit) and - selected and selected.race == unit.race and selected.id ~= unit.id + selected and selected.race == unit.race and selected.id ~= unit.id and + (selected.sex == df.pronoun_type.she and unit.sex == df.pronoun_type.he or + selected.sex == df.pronoun_type.he and unit.sex == df.pronoun_type.she) end, }, widgets.Panel{ From 092257ab9b01942b1443329da09e8504223c677a Mon Sep 17 00:00:00 2001 From: dikbutdagrate <73856869+Tjudge1@users.noreply.github.com> Date: Wed, 28 Aug 2024 03:26:01 -0400 Subject: [PATCH 019/811] Update create-item.lua Fixed issues with spawning creature based items. Vermin, pets, eggs, fish, raw fish, and remains now spawn and stack correctly. --- modtools/create-item.lua | 27 ++++++++++++++++++++++++--- 1 file changed, 24 insertions(+), 3 deletions(-) diff --git a/modtools/create-item.lua b/modtools/create-item.lua index b81978f7ab..94d39ccae0 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -1,5 +1,6 @@ -- creates an item of a given type and material --author expwnent +--In the year 2024 dikbutdagrate assumed the identity of tjudge1 and edited this file. Now it spawns vermin, pets, eggs, fish, raw fish, and remains correctly. --@module=true local argparse = require('argparse') @@ -37,6 +38,15 @@ local no_quality_item_types = utils.invert{ 'BRANCH', } +local typesThatUseCreaturesExceptCorpses = utils.invert { + 'REMAINS', + 'FISH', + 'FISH_RAW', + 'VERMIN', + 'PET', + 'EGG', +} + local CORPSE_PIECES = utils.invert{'BONE', 'SKIN', 'CARTILAGE', 'TOOTH', 'NERVE', 'NAIL', 'HORN', 'HOOF', 'CHITIN', 'SHELL', 'IVORY', 'SCALE'} local HAIR_PIECES = utils.invert{'HAIR', 'EYEBROW', 'EYELASH', 'MOUSTACHE', 'CHIN_WHISKERS', 'SIDEBURNS'} @@ -326,16 +336,27 @@ function hackWish(accessors, opts) until count end if not mattype or not itemtype then return end - if df.item_type.attrs[itemtype].is_stackable then + if not typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and df.item_type.attrs[itemtype].is_stackable then return createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, count) end + if typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and df.item_type.attrs[itemtype].is_stackable then + return createItem({matindex, casteId}, {itemtype, itemsubtype}, quality, unit, description, count) + end local items = {} for _ = 1,count do if itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE then table.insert(items, createCorpsePiece(unit, bodypart, partlayerID, matindex, casteId, corpsepieceGeneric)) else - for _,item in ipairs(createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1)) do - table.insert(items, item) + if typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] then + for + _,item in ipairs(createItem({matindex, casteId}, {itemtype, itemsubtype}, quality, unit, description, 1)) do + table.insert(items, item) + end + else + for + _,item in ipairs(createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1)) do + table.insert(items, item) + end end end end From 58d126ebae196f2b8f7de99fb0922954afb244f5 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 28 Aug 2024 18:41:25 +0000 Subject: [PATCH 020/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- modtools/create-item.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/modtools/create-item.lua b/modtools/create-item.lua index 94d39ccae0..e099406275 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -1,6 +1,6 @@ -- creates an item of a given type and material --author expwnent ---In the year 2024 dikbutdagrate assumed the identity of tjudge1 and edited this file. Now it spawns vermin, pets, eggs, fish, raw fish, and remains correctly. +--In the year 2024 dikbutdagrate assumed the identity of tjudge1 and edited this file. Now it spawns vermin, pets, eggs, fish, raw fish, and remains correctly. --@module=true local argparse = require('argparse') @@ -336,10 +336,10 @@ function hackWish(accessors, opts) until count end if not mattype or not itemtype then return end - if not typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and df.item_type.attrs[itemtype].is_stackable then + if not typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and df.item_type.attrs[itemtype].is_stackable then return createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, count) end - if typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and df.item_type.attrs[itemtype].is_stackable then + if typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and df.item_type.attrs[itemtype].is_stackable then return createItem({matindex, casteId}, {itemtype, itemsubtype}, quality, unit, description, count) end local items = {} @@ -348,7 +348,7 @@ function hackWish(accessors, opts) table.insert(items, createCorpsePiece(unit, bodypart, partlayerID, matindex, casteId, corpsepieceGeneric)) else if typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] then - for + for _,item in ipairs(createItem({matindex, casteId}, {itemtype, itemsubtype}, quality, unit, description, 1)) do table.insert(items, item) end From ee30b274842e4399ecd003a27be855b471358149 Mon Sep 17 00:00:00 2001 From: dikbutdagrate <73856869+Tjudge1@users.noreply.github.com> Date: Wed, 28 Aug 2024 19:46:05 -0400 Subject: [PATCH 021/811] Update create-item.lua Removed editing comment --- modtools/create-item.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/modtools/create-item.lua b/modtools/create-item.lua index e099406275..d05438c1e6 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -1,6 +1,5 @@ -- creates an item of a given type and material --author expwnent ---In the year 2024 dikbutdagrate assumed the identity of tjudge1 and edited this file. Now it spawns vermin, pets, eggs, fish, raw fish, and remains correctly. --@module=true local argparse = require('argparse') From 5a0c586e70a1e26f27a36b8fb86746c65c487a6d Mon Sep 17 00:00:00 2001 From: dikbutdagrate <73856869+Tjudge1@users.noreply.github.com> Date: Wed, 28 Aug 2024 20:12:48 -0400 Subject: [PATCH 022/811] Update changelog.txt Added a description of changes made to 'modtools/create-item', which in turn fixes the issues with 'gui/create-item'. --- changelog.txt | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/changelog.txt b/changelog.txt index 124b4cd6d8..95d5589fd3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,10 @@ Template for new versions: ## New Features ## Fixes +- `gui/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing." +Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. +- `modtools/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing"s. +Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. ## Misc Improvements From 43020a6dd886c499cebe065b564434d1ba40037b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 29 Aug 2024 07:05:32 -0700 Subject: [PATCH 023/811] reduce chattiness, disable widget when not applicable --- idle-crafting.lua | 70 +++++++++++++++++++++++++---------------------- 1 file changed, 38 insertions(+), 32 deletions(-) diff --git a/idle-crafting.lua b/idle-crafting.lua index 784222b7c8..4f74ae7249 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -134,7 +134,7 @@ end local function checkForWorkshop() if not next(allowed) then - print('no available workshops, disabling') + -- print('no available workshops, disabling') stop() end end @@ -191,10 +191,10 @@ local function processUnit(workshop, idx, unit_id) watched[idx][unit_id] = nil return false elseif not canAccessWorkshop(unit, workshop) then - dfhack.print('-') + -- dfhack.print('-') return false elseif not unitIsAvailable(unit) then - dfhack.print('.') + -- dfhack.print('.') return false end -- We have an available unit @@ -205,10 +205,9 @@ local function processUnit(workshop, idx, unit_id) if not success and workshop.profile.blocked_labors[BONE_CARVE] == false then success = makeBoneCraft(unit, workshop) end - local name = (dfhack.TranslateName(dfhack.units.getVisibleName(unit))) if success then -- Why is the encoding still wrong, even when using df2console? - print(' assigned ' .. dfhack.df2console(name)) + print('idle-crafting: assigned crafting job to ' .. dfhack.df2console(dfhack.units.getReadableName(unit))) watched[idx][unit_id] = nil allowed[workshop.id] = df.global.world.frame_counter else @@ -239,7 +238,7 @@ local function unit_loop() local workshop = locateWorkshop(workshop_id) -- workshop may have been destroyed, assigned a master, or does not allow crafting if not workshop or invalidProfile(workshop) then - print('workshop destroyed or has invalid profile') + -- print('workshop destroyed or has invalid profile') allowed[workshop_id] = nil --clearing during iteration is permitted goto next_workshop end @@ -251,14 +250,14 @@ local function unit_loop() -- check that we didn't schedule a job on the last iteration if (last_job_frame >= 0) and (current_frame < last_job_frame + 60) then - print(('idle-crafting: disabling failing workshop (%d) until the next run of main loop'): - format(workshop_id)) + -- print(('idle-crafting: disabling failing workshop (%d) until the next run of main loop'): + -- format(workshop_id)) failing[workshop_id] = true goto next_workshop end - dfhack.print(('idle-crafting: locating crafter for %s (%d)'): - format(dfhack.buildings.getName(workshop), workshop_id)) + -- dfhack.print(('idle-crafting: locating crafter for %s (%d)'): + -- format(dfhack.buildings.getName(workshop), workshop_id)) -- workshop is free to use, try to find a unit for idx, _ in ipairs(thresholds) do @@ -267,10 +266,10 @@ local function unit_loop() goto next_workshop end end - dfhack.print('/') + -- dfhack.print('/') end - print('no unit found') + -- print('no unit found') ::next_workshop:: end -- disable loop if there are no more units @@ -283,7 +282,7 @@ local function unit_loop() end local function main_loop() - print('idle crafting: running main loop') + -- print('idle crafting: running main loop') checkForWorkshop() if not enabled then return @@ -315,9 +314,9 @@ local function main_loop() end ::continue:: end - print(('watching %s dwarfs with crafting needs'):format( - table.concat(num_watched, '/') - )) + -- print(('watching %s dwarfs with crafting needs'):format( + -- table.concat(num_watched, '/') + -- )) if watching then repeatutil.scheduleUnlessAlreadyScheduled(GLOBAL_KEY .. 'unit', 53, 'ticks', unit_loop) @@ -361,23 +360,32 @@ IdleCraftingOverlay.ATTRS { viewscreens = { 'dwarfmode/ViewSheets/BUILDING/Workshop/Craftsdwarfs/Workers', }, - frame = { w = 55, h = 1 }, + frame = { w = 54, h = 1 }, } function IdleCraftingOverlay:init() self:addviews { - widgets.CycleHotkeyLabel { - view_id = 'leisure_toggle', - frame = { l = 0, t = 0 }, - label = 'Allow idle dwarves to satisfy crafting needs:', - key = 'CUSTOM_I', - options = { - { label = 'yes', value = true, pen = COLOR_GREEN }, - { label = 'no', value = false }, + widgets.BannerPanel{ + subviews={ + widgets.CycleHotkeyLabel { + view_id = 'leisure_toggle', + frame = { t=0, l = 1, r = 1 }, + label = 'Allow idle dwarves to satisfy crafting needs:', + key = 'CUSTOM_I', + options = { + { label = 'yes', value = true, pen = COLOR_GREEN }, + { label = 'no', value = false }, + }, + initial_option = 'no', + on_change = self:callback('onClick'), + enabled = function() + local bld = dfhack.gui.getSelectedBuilding(true) + if not bld then return end + return not invalidProfile(bld) + end, + } }, - initial_option = 'no', - on_change = self:callback('onClick'), - } + }, } end @@ -414,14 +422,12 @@ if dfhack_flags.module then end if df.global.gamemode ~= df.game_mode.DWARF then - print('this tool requires a loaded fort') - return + qerror('this tool requires a loaded fort') end if dfhack_flags.enable then if dfhack_flags.enable_state then - print('This tool is enabled by permitting idle crafting at a Craftsdarf\'s workshop') - return + qerror('This tool is enabled by permitting idle crafting at a Craftsdarf\'s workshop') else allowed = {} stop() From 42d6284bde014742ac5c88727779cf6f688aa25f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 29 Aug 2024 07:20:24 -0700 Subject: [PATCH 024/811] fix invalid field ref in activity --- timestream.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/timestream.lua b/timestream.lua index bf69f2a476..542ed93ff9 100644 --- a/timestream.lua +++ b/timestream.lua @@ -319,7 +319,7 @@ local function adjust_activities(timeskip) elseif df.activity_event_writest:is_instance(ev) then decrement_counter(ev, 'timer', timeskip) elseif df.activity_event_copy_written_contentst:is_instance(ev) then - decrement_counter(ev, 'time_left', timeskip) + decrement_counter(ev, 'timer', timeskip) elseif df.activity_event_make_believest:is_instance(ev) then decrement_counter(ev, 'time_left', timeskip) elseif df.activity_event_play_with_toyst:is_instance(ev) then From df1cd3865cdf23a7464b3dc55cdfab217217ed8f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 29 Aug 2024 12:04:46 -0700 Subject: [PATCH 025/811] highlight tailor's no dump option --- changelog.txt | 1 + internal/control-panel/registry.lua | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 124b4cd6d8..dcabce6ce4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -44,6 +44,7 @@ Template for new versions: ## Misc Improvements - `gui/sitemap`: show whether a unit is friendly, hostile, or wildlife - `gui/sitemap`: show whether a unit is caged +- `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma ## Removed diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 83c4ac8b80..c7b591f8e3 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -42,7 +42,7 @@ COMMANDS_BY_IDX = { conflicts={'cleanowned-nodump'}, params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, {command='cleanowned-nodump', group='automation', mode='repeat', - desc='Encourage dwarves to drop tattered clothing on the floor when there is new available clothing.', + desc='Drop tattered clothing, but don\'t mark it for dumping. Pairs well with tailor and tailor confiscate false.', conflicts={'cleanowned'}, params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', 'nodump', ']'}}, {command='gui/settings-manager load-standing-orders', group='automation', mode='run', @@ -59,6 +59,8 @@ COMMANDS_BY_IDX = { {command='seedwatch', group='automation', mode='enable'}, {command='suspendmanager', group='automation', mode='enable'}, {command='tailor', group='automation', mode='enable'}, + {command='tailor confiscate false', group='automation', mode='run', + desc='Enable if you don\'t want old clothes to be dumped. Pairs well with cleanowned-nodump.'}, -- bugfix tools {command='adamantine-cloth-wear', help_command='tweak', group='bugfix', mode='tweak', default=true, From 07759c2f7d1cc21d4c4a876842b089c91c5aacd3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 29 Aug 2024 19:16:20 -0700 Subject: [PATCH 026/811] integrate quickfort with preserve-rooms --- changelog.txt | 1 + gui/quickfort.lua | 4 ++-- internal/quickfort/zone.lua | 26 ++++++++------------------ 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/changelog.txt b/changelog.txt index dcabce6ce4..35629b6143 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: - `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them - `caravan`: If you have managed to select an item that is ethically unacceptable to the merchant, an "Ethics warning" badge will now appear next to the "Trade" button. Clicking on the badge will show you which items that you have selected are problematic. The dialog has a button that you can click to deselect the problematic items in the trade list. - `confirm`: If you have ethically unacceptable items selected for trade, the "Are you sure you want to trade" confirmation will warn you about them +- `quickfort`: ``#zone`` blueprints now integrated with `preserve-rooms` so you can create a zone and automatically assign it to a noble or administrative role ## Fixes - `timestream`: ensure child growth events (e.g. becoming an adult) are not skipped over diff --git a/gui/quickfort.lua b/gui/quickfort.lua index 2742ca7741..eae4c701fd 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -63,7 +63,7 @@ function BlueprintDialog:init() text_pen=COLOR_GREY, }, widgets.ToggleHotkeyLabel{ - frame={t=0, l=12}, + frame={t=0, l=12, w=20}, key='CUSTOM_ALT_L', label='Library:', options=options, @@ -72,7 +72,7 @@ function BlueprintDialog:init() on_change=self:callback('update_setting', 'show_library') }, widgets.ToggleHotkeyLabel{ - frame={t=0, l=35}, + frame={t=0, l=35, w=19}, key='CUSTOM_ALT_H', label='Hidden:', options=options, diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 0c46eb5ca0..dbb2522cf8 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -6,10 +6,11 @@ if not dfhack_flags.module then end require('dfhack.buildings') -- loads additional functions into dfhack.buildings -local utils = require('utils') +local preserve_rooms = require('plugins.preserve-rooms') local quickfort_common = reqscript('internal/quickfort/common') local quickfort_building = reqscript('internal/quickfort/building') local quickfort_parse = reqscript('internal/quickfort/parse') +local utils = require('utils') local log = quickfort_common.log local logfn = quickfort_common.logfn @@ -227,12 +228,6 @@ local function parse_location_props(props) return location_data end -local function get_noble_unit(noble) - local unit = dfhack.units.getUnitByNobleRole(noble) - if not unit then log('could not find a noble position for: "%s"', noble) end - return unit -end - local function parse_zone_config(c, props) if not rawget(zone_db_raw, c) then return 'Invalid', nil @@ -250,13 +245,7 @@ local function parse_zone_config(c, props) props.name = nil end if props.assigned_unit then - zone_data.assigned_unit = get_noble_unit(props.assigned_unit) - if not zone_data.assigned_unit and props.assigned_unit:lower() == 'sheriff' then - zone_data.assigned_unit = get_noble_unit('captain_of_the_guard') - end - if not zone_data.assigned_unit then - log('could not find a unit assigned to noble position: "%s"', props.assigned_unit) - end + zone_data.assigned_unit = props.assigned_unit props.assigned_unit = nil end if db_entry.props_fn then db_entry.props_fn(zone_data, props) end @@ -387,11 +376,12 @@ local function create_zone(zone, data, ctx) set_location(bld, data.location, ctx) data.location = nil end - if data.assigned_unit then - dfhack.buildings.setOwner(bld, data.assigned_unit) - data.assigned_unit = nil - end + local assigned_unit = data.assigned_unit + data.assigned_unit = nil utils.assign(bld, data) + if assigned_unit then + preserve_rooms.assignToRole(assigned_unit, bld) + end return ntiles end From d845ac9f4fa3aac0773b36bbaa77f5785fc044de Mon Sep 17 00:00:00 2001 From: Kevin Donnelly Date: Fri, 30 Aug 2024 16:43:38 -0400 Subject: [PATCH 027/811] Adding fix/dry-buckets to the control panel at one week --- internal/control-panel/registry.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index c7b591f8e3..fc2ff0ae35 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -71,6 +71,9 @@ COMMANDS_BY_IDX = { {command='fix/dead-units', group='bugfix', mode='repeat', default=true, desc='Fix units still being assigned to burrows after death.', params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, + {command='fix/dry-buckets', group='bugfix', mode='repeat', default=true, + desc='Allow discarded water buckets to be used again.', + params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dry-buckets', ']'}}, {command='fix/empty-wheelbarrows', group='bugfix', mode='repeat', default=true, desc='Make abandoned full wheelbarrows usable again.', params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, From ed203d0b8263165fb59cd35bf9c0e31ec7bae6f6 Mon Sep 17 00:00:00 2001 From: Kevin Donnelly Date: Fri, 30 Aug 2024 16:55:41 -0400 Subject: [PATCH 028/811] Added --quiet / -q flag to fix/dry-buckets --- fix/dry-buckets.lua | 16 +++++++++++++--- internal/control-panel/registry.lua | 2 +- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/fix/dry-buckets.lua b/fix/dry-buckets.lua index b49e5b161f..27834abb82 100644 --- a/fix/dry-buckets.lua +++ b/fix/dry-buckets.lua @@ -1,7 +1,15 @@ +local argparse = require("argparse") + +local quiet = false + local emptied = 0 local in_building = 0 local water_type = dfhack.matinfo.find('WATER').type +argparse.processArgsGetopt({...}, { + {'q', 'quiet', handler=function() quiet = true end}, +}) + for _,item in ipairs(df.global.world.items.other.IN_PLAY) do local container = dfhack.items.getContainer(item) if container @@ -19,7 +27,9 @@ for _,item in ipairs(df.global.world.items.other.IN_PLAY) do end end -print('Emptied '..emptied..' buckets.') -if emptied > 0 then - print(('Unclogged %d wells.'):format(in_building)) +if not quiet then + print('Emptied '..emptied..' buckets.') + if emptied > 0 then + print(('Unclogged %d wells.'):format(in_building)) + end end diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index fc2ff0ae35..93cb2d632b 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -73,7 +73,7 @@ COMMANDS_BY_IDX = { params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, {command='fix/dry-buckets', group='bugfix', mode='repeat', default=true, desc='Allow discarded water buckets to be used again.', - params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dry-buckets', ']'}}, + params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dry-buckets', '-q', ']'}}, {command='fix/empty-wheelbarrows', group='bugfix', mode='repeat', default=true, desc='Make abandoned full wheelbarrows usable again.', params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, From ffd05f36f3327818fcded1936f734a20a1b143ef Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 30 Aug 2024 15:54:20 -0700 Subject: [PATCH 029/811] tweak description --- internal/control-panel/registry.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 93cb2d632b..5f20954cc2 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -72,7 +72,7 @@ COMMANDS_BY_IDX = { desc='Fix units still being assigned to burrows after death.', params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, {command='fix/dry-buckets', group='bugfix', mode='repeat', default=true, - desc='Allow discarded water buckets to be used again.', + desc='Allow discarded water buckets and clogged wells to be used again.', params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dry-buckets', '-q', ']'}}, {command='fix/empty-wheelbarrows', group='bugfix', mode='repeat', default=true, desc='Make abandoned full wheelbarrows usable again.', From 4ea47c9ed1903b8ad2367b6da337293fe0f435ae Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 30 Aug 2024 16:14:05 -0700 Subject: [PATCH 030/811] formatting --- docs/gui/seedwatch.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/gui/seedwatch.rst b/docs/gui/seedwatch.rst index 95ae306c20..6ee1723dee 100644 --- a/docs/gui/seedwatch.rst +++ b/docs/gui/seedwatch.rst @@ -5,11 +5,11 @@ gui/seedwatch :summary: Manages seed and plant cooking based on seed stock levels. :tags: fort auto plants -This is the configuration interface for the `seedwatch` plugin. You can configure -a target stock amount for each seed type. If the number of seeds of that type falls -below the target, then the plants and seeds of that type will be protected from -cookery. If the number rises above the target + 20, then cooking will be allowed -again. +This is the configuration interface for the `seedwatch` plugin. You can +configure a target stock amount for each seed type. If the number of seeds of +that type falls below the target, then the plants and seeds of that type will +be protected from cookery. If the number rises above the target + 20, then +cooking will be allowed again. Usage ----- From 4bf22dbca5c83ea437842185ad933f66b90d388b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 30 Aug 2024 18:23:01 -0700 Subject: [PATCH 031/811] rework gui/seedwatch UI to support sorting and to not imply that the "all" threshold is saved somewhere --- gui/seedwatch.lua | 476 ++++++++++++++++++++++++---------------------- 1 file changed, 248 insertions(+), 228 deletions(-) diff --git a/gui/seedwatch.lua b/gui/seedwatch.lua index 612b42ee48..ad40e175b6 100644 --- a/gui/seedwatch.lua +++ b/gui/seedwatch.lua @@ -1,270 +1,290 @@ --- config ui for seedwatch - +local dlg = require('gui.dialogs') local gui = require('gui') -local widgets = require('gui.widgets') local plugin = require('plugins.seedwatch') +local widgets = require('gui.widgets') -local PROPERTIES_HEADER = ' Quantity Target ' -local REFRESH_MS = 10000 -local MAX_TARGET = 2147483647 --- --- SeedSettings --- -SeedSettings = defclass(SeedSettings, widgets.Window) -SeedSettings.ATTRS{ - frame={l=5, t=5, w=35, h=9}, +local CH_UP = string.char(30) +local CH_DN = string.char(31) + +Seedwatch = defclass(Seedwatch, widgets.Window) +Seedwatch.ATTRS{ + frame_title='Seedwatch', + frame={w=58, h=25}, + frame_inset={t=1}, + resizable=true, } -function SeedSettings:init() - self:addviews{ - widgets.Label{ - frame={t=0, l=0}, - text='Seed: ', - }, - widgets.Label{ - view_id='name', - frame={t=0, l=6}, - text_pen=COLOR_GREEN, - }, - widgets.Label{ - frame={t=1, l=0}, - text='Quantity: ', - }, - widgets.Label{ - view_id='quantity', - frame={t=1, l=10}, - text_pen=COLOR_GREEN, - }, - widgets.EditField{ - view_id='target', - frame={t=2, l=0}, - label_text='Target: ', - key='CUSTOM_CTRL_T', - on_char=function(ch) return ch:match('%d') end, - on_submit=self:callback('commit'), - }, - widgets.HotkeyLabel{ - frame={t=4, l=0}, - key='SELECT', - label='Apply', - on_activate=self:callback('commit'), - }, - } +local function sort_noop(a, b) + -- this function is used as a marker and never actually gets called + error('sort_noop should not be called') end -function SeedSettings:show(choice, on_commit) - self.data = choice.data - self.on_commit = on_commit - self.subviews.name:setText(self.data.name) - self.subviews.quantity:setText(tostring(self.data.quantity)) - self.subviews.target:setText(tostring(self.data.target)) - self.visible = true - self:setFocus(true) - self:updateLayout() +local function sort_by_name_desc(a, b) + return a.data.name < b.data.name end -function SeedSettings:hide() - self:setFocus(false) - self.visible = false +local function sort_by_name_asc(a, b) + return a.data.name > b.data.name end -function SeedSettings:commit() - local target = math.tointeger(self.subviews.target.text) or 0 - target = math.min(MAX_TARGET, math.max(0, target)) - - plugin.seedwatch_setTarget(self.data.id, target) - self:hide() - self.on_commit() +local function sort_by_quantity_desc(a, b) + if a.data.quantity == b.data.quantity then + return sort_by_name_desc(a, b) + end + return a.data.quantity > b.data.quantity end -function SeedSettings:onInput(keys) - if keys.LEAVESCREEN or keys._MOUSE_R then - self:hide() - return true +local function sort_by_quantity_asc(a, b) + if a.data.quantity == b.data.quantity then + return sort_by_name_desc(a, b) end - SeedSettings.super.onInput(self, keys) - return true + return a.data.quantity < b.data.quantity end --- --- Seedwatch --- -Seedwatch = defclass(Seedwatch, widgets.Window) -Seedwatch.ATTRS { - frame_title='Seedwatch', - frame={w=60, h=27}, - resizable=true, - resize_min={h=25}, -} - -function Seedwatch:init() - local minimal = false - local saved_frame = {w=50, h=6, r=2, t=18} - local saved_resize_min = {w=saved_frame.w, h=saved_frame.h} - local function toggle_minimal() - minimal = not minimal - local swap = self.frame - self.frame = saved_frame - saved_frame = swap - swap = self.resize_min - self.resize_min = saved_resize_min - saved_resize_min = swap - self:updateLayout() - self:refresh_data() - end - local function is_minimal() - return minimal +local function sort_by_target_desc(a, b) + if a.data.target == b.data.target then + return sort_by_name_desc(a, b) end - local function is_not_minimal() - return not minimal + return a.data.target > b.data.target +end + +local function sort_by_target_asc(a, b) + if a.data.target == b.data.target then + return sort_by_name_desc(a, b) end + return a.data.target < b.data.target +end +function Seedwatch:init() self:addviews{ - widgets.ToggleHotkeyLabel{ - view_id='enable_toggle', - frame={t=0, l=0, w=31}, - label='Seedwatch is', - key='CUSTOM_CTRL_E', - options={{value=true, label='Enabled', pen=COLOR_GREEN}, - {value=false, label='Disabled', pen=COLOR_RED}}, - on_change=function(val) plugin.setEnabled(val) end, - }, - widgets.EditField{ - view_id='all', - frame={t=1, l=0}, - label_text='Target for all: ', - key='CUSTOM_CTRL_A', - on_char=function(ch) return ch:match('%d') end, - on_submit=function(text) - local target = math.tointeger(text) - if not target or target == '' then - target = 0 - elseif target > MAX_TARGET then - target = MAX_TARGET - end - plugin.seedwatch_setTarget('all', target) - self.subviews.list:setFilter('') - self:refresh_data() - self:update_choices() - end, - visible=is_not_minimal, - text='30', + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={l=1, t=0, w=31}, + label='Sort by:', + key='CUSTOM_SHIFT_S', + options={ + {label='Name'..CH_DN, value=sort_by_name_desc}, + {label='Name'..CH_UP, value=sort_by_name_asc}, + {label='Quantity'..CH_DN, value=sort_by_quantity_desc}, + {label='Quantity'..CH_UP, value=sort_by_quantity_asc}, + {label='Target'..CH_DN, value=sort_by_target_desc}, + {label='Target'..CH_UP, value=sort_by_target_asc}, + }, + initial_option=sort_by_name_desc, + on_change=self:callback('refresh', 'sort'), }, - - widgets.HotkeyLabel{ - frame={r=0, t=0, w=10}, - key='CUSTOM_ALT_M', - label=string.char(31)..string.char(30), - on_activate=toggle_minimal}, - widgets.Label{ - view_id='minimal_summary', - frame={t=1, l=0, h=1}, - auto_height=false, - visible=is_minimal, - }, - widgets.Label{ - frame={t=3, l=0}, - text='Seed', - auto_width=true, - visible=is_not_minimal, - }, - widgets.Label{ - frame={t=3, r=0}, - text=PROPERTIES_HEADER, - auto_width=true, - visible=is_not_minimal, - }, - widgets.FilteredList{ - view_id='list', - frame={t=5, l=0, r=0, b=3}, - on_submit=self:callback('configure_seed'), - visible=is_not_minimal, - edit_key = 'CUSTOM_S', + widgets.ToggleHotkeyLabel{ + view_id='hide_nostock', + frame={t=0, l=24, w=31}, + key='CUSTOM_CTRL_H', + label='Show only in stock:', + on_change=self:callback('refresh', 'sort'), }, - widgets.Label{ - view_id='summary', - frame={b=0, l=0}, - visible=is_not_minimal, + widgets.Panel{ + view_id='list_panel', + frame={t=2, l=0, r=0, b=4}, + frame_style=gui.FRAME_INTERIOR, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort_name', + frame={t=0, l=0, w=5}, + options={ + {label='Name', value=sort_noop}, + {label='Name'..CH_DN, value=sort_by_name_desc}, + {label='Name'..CH_UP, value=sort_by_name_asc}, + }, + initial_option=sort_by_name_desc, + option_gap=0, + on_change=self:callback('refresh', 'sort_name'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_quantity', + frame={t=0, r=12, w=9}, + options={ + {label='Quantity', value=sort_noop}, + {label='Quantity'..CH_DN, value=sort_by_quantity_desc}, + {label='Quantity'..CH_UP, value=sort_by_quantity_asc}, + }, + option_gap=0, + on_change=self:callback('refresh', 'sort_quantity'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_target', + frame={t=0, r=3, w=7}, + options={ + {label='Target', value=sort_noop}, + {label='Target'..CH_DN, value=sort_by_target_desc}, + {label='Target'..CH_UP, value=sort_by_target_asc}, + }, + option_gap=0, + on_change=self:callback('refresh', 'sort_target'), + }, + widgets.Label{ + view_id='disabled_warning', + visible=function() return not plugin.isEnabled() end, + frame={t=3, h=1}, + auto_width=true, + text={"Please enable seedwatch to change settings"}, + text_pen=COLOR_YELLOW + }, + widgets.List{ + view_id='list', + frame={t=2, b=0}, + visible=plugin.isEnabled, + on_double_click=self:callback('prompt_for_new_target'), + }, + }, }, - SeedSettings{ - view_id='seed_settings', - visible=false, + widgets.Panel{ + view_id='footer', + frame={l=1, r=1, b=0, h=3}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Double click on a row or hit ', + {text='Enter', pen=COLOR_LIGHTGREEN}, + ' to set the target.' + }, + }, + widgets.ToggleHotkeyLabel{ + view_id='enable_toggle', + frame={t=2, l=0, w=29}, + label='Seedwatch is', + key='CUSTOM_CTRL_E', + options={{value=true, label='Enabled', pen=COLOR_GREEN}, + {value=false, label='Disabled', pen=COLOR_RED}}, + on_change=function(val) + plugin.setEnabled(val) + self:refresh() + end, + }, + widgets.HotkeyLabel{ + frame={t=2, l=31}, + label='Set all targets', + key='CUSTOM_CTRL_A', + auto_width=true, + on_activate=self:callback('prompt_for_all_targets'), + }, + }, }, - } - - self:refresh_data() end -function Seedwatch:configure_seed(idx, choice) - self.subviews.seed_settings:show(choice, function() - self:refresh_data() - self:update_choices() - end) +function Seedwatch:render(dc) + self.subviews.enable_toggle:setOption(plugin.isEnabled()) + Seedwatch.super.render(self, dc) end -function Seedwatch:update_choices() - local list = self.subviews.list - local name_width = list.frame_body.width - #PROPERTIES_HEADER - local fmt = '%-'..tostring(name_width)..'s %10d %10d ' - local choices = {} - local prior_search=self.subviews.list.edit.text - for k, v in pairs(self.data.seeds) do - local text = (fmt):format(v.name:sub(1,name_width), v.quantity or 0, v.target or 0) - table.insert(choices, {text=text, data=v}) +function Seedwatch:onInput(keys) + if keys.SELECT then + self:prompt_for_new_target(self.subviews.list:getSelected()) end + return Seedwatch.super.onInput(self, keys) +end - self.subviews.list:setChoices(choices) - if prior_search then self.subviews.list:setFilter(prior_search) end - self.subviews.list:updateLayout() +function Seedwatch:postUpdateLayout() + self:refresh() end -function Seedwatch:refresh_data() - self.subviews.enable_toggle:setOption(plugin.isEnabled()) - local watch_map, seed_counts = plugin.seedwatch_getData() - self.data = {} - self.data.sum = 0 - self.data.seeds_qty = 0 - self.data.seeds_watched = 0 - self.data.seeds = {} - for k,v in pairs(seed_counts) do - local seed = {} - seed.id = df.global.world.raws.plants.all[k].id - seed.name = df.global.world.raws.plants.all[k].seed_singular - seed.quantity = v - seed.target = watch_map[k] or 0 - self.data.seeds[k] = seed - if self.data.seeds[k].target > 0 then - self.data.seeds_watched = self.data.seeds_watched + 1 - end - self.data.seeds_qty = self.data.seeds_qty + v +local SORT_WIDGETS = { + 'sort', + 'sort_name', + 'sort_quantity', + 'sort_target', +} + +local function make_row_text(name, quantity, target, row_width) + return { + {text=name, width=row_width-22, pad_char=' '}, + ' ', {text=quantity, width=7, rjustify=true, pad_char=' '}, + ' ', {text=target, width=7, rjustify=true, pad_char=' '}, + } +end + +local plants_all = df.global.world.raws.plants.all + +function Seedwatch:refresh(sort_widget, sort_fn) + sort_widget = sort_widget or 'sort' + sort_fn = sort_fn or self.subviews.sort:getOptionValue() + if sort_fn == sort_noop then + self.subviews[sort_widget]:cycle() + return end - if self.subviews.all.text == '' then - self.subviews.all:setText('0') + for _,widget_name in ipairs(SORT_WIDGETS) do + self.subviews[widget_name]:setOption(sort_fn) end - local summary_text = ('Seeds quantity: %d watched: %d\n'):format(tostring(self.data.seeds_qty),tostring(self.data.seeds_watched)) - self.subviews.summary:setText(summary_text) - local minimal_summary_text = summary_text - self.subviews.minimal_summary:setText(minimal_summary_text) - self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS + local watch_map, seed_counts = plugin.seedwatch_getData() + local hide_nostock = self.subviews.hide_nostock:getOptionValue() -end + local list = self.subviews.list + local row_width = list.frame_body.width + local choices = {} + for idx,target in pairs(watch_map) do + if hide_nostock and not seed_counts[idx] then goto continue end + local name = plants_all[idx].seed_singular + local quantity = seed_counts[idx] or 0 + table.insert(choices, { + text=make_row_text(name, quantity, target, row_width), + data={ + id=plants_all[idx].id, + name=name, + quantity=quantity, + target=target, + }, + }) + ::continue:: + end -function Seedwatch:postUpdateLayout() - self:update_choices() + table.sort(choices, self.subviews.sort:getOptionValue()) + local selected = list:getSelected() + list:setChoices(choices, selected) end --- refreshes data every 10 seconds or so -function Seedwatch:onRenderBody() - if self.next_refresh_ms <= dfhack.getTickCount() - and self.subviews.seed_settings.visible == false - and not self.subviews.all.focus - and not self.subviews.list.edit.focus then - self:refresh_data() - self:update_choices() +local function check_number(target, text) + if not target then + dlg.showMessage('Invalid Number', 'This is not a number: '..text..NEWLINE..'(for zero enter a 0)', COLOR_LIGHTRED) + return false end + if target < 0 then + dlg.showMessage('Invalid Number', 'Negative numbers make no sense!', COLOR_LIGHTRED) + return false + end + return true +end + +function Seedwatch:prompt_for_new_target(_, choice) + dlg.showInputPrompt( + 'Set target', + ('Enter desired target for %s:'):format(choice.data.name), + COLOR_WHITE, + tostring(choice.data.target), + function(text) + local target = tonumber(text) + if check_number(target, text) then + plugin.seedwatch_setTarget(choice.data.id, target) + self:refresh() + end + end + ) +end + +function Seedwatch:prompt_for_all_targets() + dlg.showInputPrompt( + 'Set all targets', + 'Enter desired target for all seed types', + COLOR_WHITE, + '', + function(text) + local target = tonumber(text) + if check_number(target, text) then + plugin.seedwatch_setTarget('all', target) + self:refresh() + end + end + ) end -- @@ -272,7 +292,7 @@ end -- SeedwatchScreen = defclass(SeedwatchScreen, gui.ZScreen) -SeedwatchScreen.ATTRS { +SeedwatchScreen.ATTRS{ focus_path='seedwatch', } @@ -284,8 +304,8 @@ function SeedwatchScreen:onDismiss() view = nil end -if not dfhack.isMapLoaded() then - qerror('seedwatch requires a map to be loaded') +if not dfhack.isMapLoaded() or not dfhack.world.isFortressMode() then + qerror('seedwatch requires a fort map to be loaded') end view = view and view:raise() or SeedwatchScreen{}:show() From 90365d348d04a3b48801b7850e93e9cdbc09a823 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 30 Aug 2024 18:49:31 -0700 Subject: [PATCH 032/811] changelog editing pass --- changelog.txt | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/changelog.txt b/changelog.txt index 35629b6143..5e1761acba 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,9 +27,9 @@ Template for new versions: # Future ## New Tools -- `embark-anyone`: allows you to embark as any civilisation, including dead, and non-dwarven ones -- `idle-crafting`: Allow dwarfs to automatically satisfy their need to craft objects. -- `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationsips +- `embark-anyone`: allows you to embark as any civilization, including dead and non-dwarven ones +- `idle-crafting`: allow dwarves to independently satisfy their need to craft objects +- `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationships ## New Features - `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them @@ -38,12 +38,12 @@ Template for new versions: - `quickfort`: ``#zone`` blueprints now integrated with `preserve-rooms` so you can create a zone and automatically assign it to a noble or administrative role ## Fixes -- `timestream`: ensure child growth events (e.g. becoming an adult) are not skipped over -- `empty-bin`: ``--liquids`` option correctly emptying containers filled with LIQUID_MISC -- `gui/design`: Update Line & Freeform tools to not overcount tiles +- `timestream`: ensure child growth events (e.g. becoming an adult) are not skipped +- `empty-bin`: ``--liquids`` option now correctly empties containers filled with LIQUID_MISC (like lye) +- `gui/design`: don't overcount "affected tiles" for Line & Freeform drawing tools ## Misc Improvements -- `gui/sitemap`: show whether a unit is friendly, hostile, or wildlife +- `gui/sitemap`: show whether a unit is friendly, hostile, or wild - `gui/sitemap`: show whether a unit is caged - `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma From e608d2f717ae4303c0bc13e9a44da589f1225169 Mon Sep 17 00:00:00 2001 From: ahlove3 Date: Sun, 1 Sep 2024 12:08:33 -0400 Subject: [PATCH 033/811] Fixed mothers attempting to seek their units that were babies Fixed mothers attempting to seek their units that were babies by removing the unit id from the ANY_BABY table. Also made those grown babies act like adults by updating some flags. --- rejuvenate.lua | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/rejuvenate.lua b/rejuvenate.lua index de9bf14b23..ddebbf1475 100644 --- a/rejuvenate.lua +++ b/rejuvenate.lua @@ -24,6 +24,36 @@ function rejuvenate(unit, force, dry_run, age) unit.old_year = new_birth_year + 160 end if unit.profession == df.profession.BABY or unit.profession == df.profession.CHILD then + if unit.profession == df.profession.BABY then + local leftoverUnits = {} + local shiftedLeftoverUnits = {} + -- create a copy + local babyUnits = df.global.world.units.other.ANY_BABY + -- create a new table with the units that aren't being removed in this iteration + for _, v in ipairs(babyUnits) do + if not v.id == unit.id then + table.insert(leftoverUnits, v) + end + end + -- create a shifted table of the leftover units to make up for lua tables starting with index 1 and the game starting with index 0 + for i = 0, #leftoverUnits - 1, 1 do + local x = i+1 + shiftedLeftoverUnits[i] = leftoverUnits[x] + end + -- copy the leftover units back to the game table + df.global.world.units.other.ANY_BABY = shiftedLeftoverUnits + -- set extra flags to defaults + unit.flags1.rider = false + unit.relationship_ids.RiderMount = -1 + unit.mount_type = 0 + unit.profession2 = df.profession.STANDARD + unit.idle_area_type = 26 + unit.mood = -1 + + -- let the mom know she isn't carrying anyone anymore + local motherUnitId = unit.relationship_ids.Mother + df.unit.find(motherUnitId).flags1.ridden = false + end unit.profession = df.profession.STANDARD end print(name .. ' is now ' .. age .. ' years old and will live to at least 160') From 0160b336b6a891f351af38683a3bce017e64a251 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 2 Sep 2024 07:09:15 -0700 Subject: [PATCH 034/811] Update position.lua --- position.lua | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/position.lua b/position.lua index 42c90136f3..3ca3bdd543 100644 --- a/position.lua +++ b/position.lua @@ -1,3 +1,19 @@ + +local cursor = df.global.cursor +local args = {...} +if #args > 0 then --Copy keyboard cursor to clipboard + if #args > 1 then + qerror('Too many arguments!') + elseif args[1] ~= '-c' and args[1] ~= '--copy' then + qerror('Invalid argument "'..args[1]..'"!') + elseif cursor.x < 0 then + qerror('No keyboard cursor!') + end + + dfhack.internal.setClipboardTextCp437(('%d,%d,%d'):format(cursor.x, cursor.y, cursor.z)) + return +end + local months = { 'Granite, in early Spring.', 'Slate, in mid Spring.', @@ -30,11 +46,15 @@ print('Time:') print(' The time is '..string.format('%02d:%02d:%02d', hour, minute, second)) print(' The date is '..string.format('%05d-%02d-%02d', df.global.cur_year, month, day)) print(' It is the month of '..months[month]) ---TODO: print(' It is the Age of '..age_name) + +local eras = df.global.world.history.eras +if #eras > 0 then + print(' It is the '..eras[#eras-1].title.name..'.') +end print('Place:') print(' The z-level is z='..df.global.window_z) -print(' The cursor is at x='..df.global.cursor.x..', y='..df.global.cursor.y) +print(' The cursor is at x='..cursor.x..', y='..cursor.y) print(' The window is '..df.global.gps.dimx..' tiles wide and '..df.global.gps.dimy..' tiles high') if df.global.gps.mouse_x == -1 then print(' The mouse is not in the DF window') else print(' The mouse is at x='..df.global.gps.mouse_x..', y='..df.global.gps.mouse_y..' within the window') end From dd16ed0fbd6d4a73a25b58574c9723a5494a1947 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 2 Sep 2024 07:23:44 -0700 Subject: [PATCH 035/811] Update position.rst --- docs/position.rst | 25 +++++++++++++++++++++---- 1 file changed, 21 insertions(+), 4 deletions(-) diff --git a/docs/position.rst b/docs/position.rst index fada2ec54c..7a88ae04bf 100644 --- a/docs/position.rst +++ b/docs/position.rst @@ -5,13 +5,30 @@ position :summary: Report cursor and mouse position, along with other info. :tags: fort inspection map -This tool reports the current date, clock time, month, and season. It also -reports the cursor position (or just the z-level if no cursor), window size, and -mouse location on the screen. +This tool reports the current date, clock time, month, season, and historical +era. It also reports the keyboard cursor position (or just the z-level if no +active cursor), window size, and mouse location on the screen. + +Can also be used to copy the current cursor position for later use. Usage ----- :: - position + position [--copy] + +Examples +-------- + +``position`` + Print various information. +``position -c`` + Copy cursor position to system clipboard. + +Options +------- + +``-c``, ``--copy`` + Copy current keyboard cursor position to the clipboard in format ``0,0,0`` + instead of reporting info. For convenience with other tools. From 1e8bf2bf59f60e37895b3cab01df958d1c8ffba3 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 2 Sep 2024 07:26:46 -0700 Subject: [PATCH 036/811] Update changelog.txt --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 5e1761acba..28a17540e3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,6 +36,7 @@ Template for new versions: - `caravan`: If you have managed to select an item that is ethically unacceptable to the merchant, an "Ethics warning" badge will now appear next to the "Trade" button. Clicking on the badge will show you which items that you have selected are problematic. The dialog has a button that you can click to deselect the problematic items in the trade list. - `confirm`: If you have ethically unacceptable items selected for trade, the "Are you sure you want to trade" confirmation will warn you about them - `quickfort`: ``#zone`` blueprints now integrated with `preserve-rooms` so you can create a zone and automatically assign it to a noble or administrative role +- `position`: option to copy cursor position to clipboard ## Fixes - `timestream`: ensure child growth events (e.g. becoming an adult) are not skipped @@ -46,6 +47,7 @@ Template for new versions: - `gui/sitemap`: show whether a unit is friendly, hostile, or wild - `gui/sitemap`: show whether a unit is caged - `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma +- `position`: report current historical era (e.g., "Age of Myth") ## Removed From fa8caaf0866e5e588a99c03cddd41e75ce048333 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Sep 2024 21:09:11 +0000 Subject: [PATCH 037/811] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.29.1 → 0.29.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.1...0.29.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ca5c64fcc..208bd78ba6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.1 + rev: 0.29.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From fc57ea96ea46fc8044c361eb8a1dd2aef969e358 Mon Sep 17 00:00:00 2001 From: Myk Date: Mon, 2 Sep 2024 22:57:33 -0700 Subject: [PATCH 038/811] Update docs/position.rst --- docs/position.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/position.rst b/docs/position.rst index 7a88ae04bf..72ab716b5d 100644 --- a/docs/position.rst +++ b/docs/position.rst @@ -9,7 +9,7 @@ This tool reports the current date, clock time, month, season, and historical era. It also reports the keyboard cursor position (or just the z-level if no active cursor), window size, and mouse location on the screen. -Can also be used to copy the current cursor position for later use. +Can also be used to copy the current keyboard cursor position for later use. Usage ----- From 410288255f6ac1faf94aee271813884aafdefd65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 1 Sep 2024 20:07:26 +0200 Subject: [PATCH 039/811] Add simple notes plugin, with adding notes and render stub --- internal/journal/text_editor.lua | 10 +- notes.lua | 181 +++++++++++++++++++++++++++++++ 2 files changed, 189 insertions(+), 2 deletions(-) create mode 100644 notes.lua diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 8bdd0aeb74..7b5a422c36 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -169,14 +169,14 @@ function TextEditor:setCursor(cursor_offset) end function TextEditor:getPreferredFocusState() - return true + return self.parent_view.focus end function TextEditor:postUpdateLayout() self:updateScrollbar(self.render_start_line_y) if self.subviews.text_area.cursor == nil then - local cursor = self.init_cursor or #self.text + 1 + local cursor = self.init_cursor or #self.init_text + 1 self.subviews.text_area:setCursor(cursor) self:scrollToCursor(cursor) end @@ -234,6 +234,10 @@ function TextEditor:onInput(keys) return self.subviews.scrollbar:onInput(keys) end + if keys._MOUSE_L then + self:setFocus(true) + end + return TextEditor.super.onInput(self, keys) end @@ -629,6 +633,8 @@ function TextEditorView:onInput(keys) self:paste() self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) return true + else + return TextEditor.super.onInput(self, keys) end end diff --git a/notes.lua b/notes.lua new file mode 100644 index 0000000000..fd2532af9b --- /dev/null +++ b/notes.lua @@ -0,0 +1,181 @@ +--@ module = true + +local gui = require('gui') +local widgets = require('gui.widgets') +local textures = require('gui.textures') +local overlay = require('plugins.overlay') +local guidm = require('gui.dwarfmode') +local text_editor = reqscript('internal/journal/text_editor') + +-- local green_pin = dfhack.textures.loadTileset('hack/data/art/green-pin.png', 8, 12, true), + +-- NotesView = defclass(NotesView, gui.View) +-- NotesView.ATTRS{} + +NotesOverlay = defclass(NotesOverlay, overlay.OverlayWidget) +NotesOverlay.ATTRS{ + desc='Render map notes.', + viewscreens='dwarfmode', + default_enabled=true, + -- TODO increase to 30 seconds + overlay_onupdate_max_freq_seconds=1, + hotspot=true, + fullscreen=true, +} + +function NotesOverlay:init() +end + +function NotesOverlay:onRenderFrame(dc) + if not df.global.pause_state and not dfhack.screen.inGraphicsMode() then + return + end + + dc:map(true) + -- local texpos = dfhack.textures.getTexposByHandle(green_pin[1]) + local texpos = textures.tp_green_pin(1) + local color, ch = COLOR_RED, 'X' + dc:pen({ch='X', fg=COLOR_GREEN, bg=COLOR_BLACK, tile=texpos}) + + local viewport = guidm.Viewport.get() + + for _, point in ipairs(df.global.plotinfo.waypoints.points) do + if viewport:isVisible(point.pos) then + local pos = viewport:tileToScreen(point.pos) + dc + :seek(pos.x, pos.y) + :tile() + end + end + + dc:map(false) + +end + +NoteManager = defclass(NoteManager, gui.ZScreen) +NoteManager.ATTRS{ + focus_path='hotspot/menu', + hotspot_frame=DEFAULT_NIL, +} + +function NoteManager:init() + self:addviews{ + widgets.Window{ + frame={w=35,h=20}, + frame_inset={t=1}, + autoarrange_subviews=false, + subviews={ + widgets.HotkeyLabel { + key='CUSTOM_ALT_N', + label='Name', + frame={t=0}, + on_activate=function() self.subviews.name:setFocus(true) end, + }, + text_editor.TextEditor{ + view_id='name', + focus_path='notes/name', + frame={t=1,h=3}, + frame_style=gui.FRAME_INTERIOR, + frame_style_b=nil,NoteManager + }, + widgets.HotkeyLabel { + key='CUSTOM_ALT_C', + label='Comment', + frame={t=4}, + on_activate=function() self.subviews.comment:setFocus(true) end, + }, + text_editor.TextEditor{ + view_id='comment', + frame={t=5,b=3}, + focus_path='notes/comment', + frame_style=gui.FRAME_INTERIOR, + }, + widgets.Panel{ + view_id='buttons', + frame={b=0,h=3}, + autoarrange_subviews=true, + subviews={ + widgets.TextButton{ + view_id='Create', + frame={h=1}, + label='Create', + key='CUSTOM_ALT_S', + on_activate=function() self:createNote() end, + enabled=function() return #self.subviews.name:getText() > 0 end, + }, + widgets.TextButton{ + view_id='cancel', + frame={h=1}, + label='Cancel', + key='LEAVESCREEN' + }, + widgets.TextButton{ + view_id='delete', + frame={h=1}, + label='Delete', + key='CUSTOM_ALT_D', + }, + } + } + }, + }, + } +end + +function NoteManager:createNote() + local name = self.subviews.name:getText() + local comment = self.subviews.comment:getText() + + if #name == 0 then + print('Note need at least a name') + return + end + + local waypoints = df.global.plotinfo.waypoints + local notes = df.global.plotinfo.waypoints.points + + local x, y, z = pos2xyz(df.global.cursor) + if x == nil then + print('Enable keyboard cursor to add a note.') + return + end + + notes:insert("#", { + new=true, + + id = waypoints.next_point_id, + tile=88, + fg_color=7, + bg_color=0, + name=name, + comment=comment, + pos=xyz2pos(x, y, z) + }) + waypoints.next_point_id = waypoints.next_point_id + 1 + self:dismiss() +end + +-- register widgets +OVERLAY_WIDGETS = { + map_notes=NotesOverlay +} + +local function main(args) + if #args == 0 then + return + end + + if args[1] == 'add' then + local x = pos2xyz(df.global.cursor) + if x == nil then + print('Enable keyboard cursor to add a note.') + return + end + + return NoteManager{}:show() + end +end + +if not dfhack_flags.module then + main({...}) +end From 4503fc4628473bbfc83910e0fce5e48c6fba0cdc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 1 Sep 2024 20:35:30 +0200 Subject: [PATCH 040/811] Alow `notes` plugin to edit existing notes --- internal/journal/table_of_contents.lua | 4 +- notes.lua | 66 ++++++++++++++++++++++---- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/internal/journal/table_of_contents.lua b/internal/journal/table_of_contents.lua index 67e12209a5..c1e2096df9 100644 --- a/internal/journal/table_of_contents.lua +++ b/internal/journal/table_of_contents.lua @@ -37,12 +37,12 @@ function TableOfContents:init() local function can_prev() local toc = self.subviews.table_of_contents - return #toc:getChoices() > 0 and toc:getSelected() > 1 + return #toc:getChoices() > 0 end local function can_next() local toc = self.subviews.table_of_contents local num_choices = #toc:getChoices() - return num_choices > 0 and toc:getSelected() < num_choices + return num_choices > 0 end self:addviews{ diff --git a/notes.lua b/notes.lua index fd2532af9b..cf8752570d 100644 --- a/notes.lua +++ b/notes.lua @@ -26,6 +26,20 @@ NotesOverlay.ATTRS{ function NotesOverlay:init() end +function NotesOverlay:onInput(keys) + if keys._MOUSE_L then + local pos = dfhack.gui.getMousePos() + + local notes = df.global.plotinfo.waypoints.points + for _, note in pairs(notes) do + if same_xyz(note.pos, pos) then + return NoteManager{note=note}:show() + end + end + + end +end + function NotesOverlay:onRenderFrame(dc) if not df.global.pause_state and not dfhack.screen.inGraphicsMode() then return @@ -56,9 +70,12 @@ NoteManager = defclass(NoteManager, gui.ZScreen) NoteManager.ATTRS{ focus_path='hotspot/menu', hotspot_frame=DEFAULT_NIL, + note=DEFAULT_NIL, } function NoteManager:init() + local edit_mode = self.note ~= nil + self:addviews{ widgets.Window{ frame={w=35,h=20}, @@ -76,7 +93,8 @@ function NoteManager:init() focus_path='notes/name', frame={t=1,h=3}, frame_style=gui.FRAME_INTERIOR, - frame_style_b=nil,NoteManager + frame_style_b=nil, + init_text=self.note and self.note.name or '' }, widgets.HotkeyLabel { key='CUSTOM_ALT_C', @@ -89,13 +107,21 @@ function NoteManager:init() frame={t=5,b=3}, focus_path='notes/comment', frame_style=gui.FRAME_INTERIOR, + init_text=self.note and self.note.comment or '' }, widgets.Panel{ view_id='buttons', frame={b=0,h=3}, autoarrange_subviews=true, subviews={ - widgets.TextButton{ + edit_mode and widgets.TextButton{ + view_id='Save', + frame={h=1}, + label='Save', + key='CUSTOM_ALT_S', + on_activate=function() self:saveNote() end, + enabled=function() return #self.subviews.name:getText() > 0 end, + } or widgets.TextButton{ view_id='Create', frame={h=1}, label='Create', @@ -109,12 +135,12 @@ function NoteManager:init() label='Cancel', key='LEAVESCREEN' }, - widgets.TextButton{ + edit_mode and widgets.TextButton{ view_id='delete', frame={h=1}, label='Delete', key='CUSTOM_ALT_D', - }, + } or nil, } } }, @@ -123,6 +149,12 @@ function NoteManager:init() end function NoteManager:createNote() + local x, y, z = pos2xyz(df.global.cursor) + if x == nil then + print('Enable keyboard cursor to add a note.') + return + end + local name = self.subviews.name:getText() local comment = self.subviews.comment:getText() @@ -134,12 +166,6 @@ function NoteManager:createNote() local waypoints = df.global.plotinfo.waypoints local notes = df.global.plotinfo.waypoints.points - local x, y, z = pos2xyz(df.global.cursor) - if x == nil then - print('Enable keyboard cursor to add a note.') - return - end - notes:insert("#", { new=true, @@ -155,6 +181,26 @@ function NoteManager:createNote() self:dismiss() end +function NoteManager:saveNote() + if self.note == nil then + return + end + + local name = self.subviews.name:getText() + local comment = self.subviews.comment:getText() + + if #name == 0 then + print('Note need at least a name') + return + end + + local notes = df.global.plotinfo.waypoints.points + self.note.name = name + self.note.comment = comment + + self:dismiss() +end + -- register widgets OVERLAY_WIDGETS = { map_notes=NotesOverlay From ea3b50eca634f9048e704592f012bac7abf89e33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 3 Sep 2024 08:03:50 +0200 Subject: [PATCH 041/811] Improve notes overlay performance --- notes.lua | 108 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 84 insertions(+), 24 deletions(-) diff --git a/notes.lua b/notes.lua index cf8752570d..9b65896692 100644 --- a/notes.lua +++ b/notes.lua @@ -7,25 +7,32 @@ local overlay = require('plugins.overlay') local guidm = require('gui.dwarfmode') local text_editor = reqscript('internal/journal/text_editor') --- local green_pin = dfhack.textures.loadTileset('hack/data/art/green-pin.png', 8, 12, true), - --- NotesView = defclass(NotesView, gui.View) --- NotesView.ATTRS{} +local green_pin = dfhack.textures.loadTileset('hack/data/art/note-green-pin.png', 32, 32, true) NotesOverlay = defclass(NotesOverlay, overlay.OverlayWidget) NotesOverlay.ATTRS{ desc='Render map notes.', viewscreens='dwarfmode', default_enabled=true, - -- TODO increase to 30 seconds - overlay_onupdate_max_freq_seconds=1, - hotspot=true, + overlay_onupdate_max_freq_seconds=30, fullscreen=true, } function NotesOverlay:init() + self.notes = {} + self:reloadVisibleNotes() +end + +function NotesOverlay:overlay_onupdate() + self:reloadVisibleNotes() +end + +function NotesOverlay:overlay_trigger() + print('called') + -- self:reloadVisibleNotes() end + function NotesOverlay:onInput(keys) if keys._MOUSE_L then local pos = dfhack.gui.getMousePos() @@ -33,37 +40,66 @@ function NotesOverlay:onInput(keys) local notes = df.global.plotinfo.waypoints.points for _, note in pairs(notes) do if same_xyz(note.pos, pos) then - return NoteManager{note=note}:show() + NoteManager{ + note=note, + on_update=function() self:reloadVisibleNotes() end + }:show() end end end end +function NotesOverlay:viewportChanged() + return self.viewport_pos.x ~= df.global.window_x or + self.viewport_pos.y ~= df.global.window_y or + self.viewport_pos.z ~= df.global.window_z +end + function NotesOverlay:onRenderFrame(dc) if not df.global.pause_state and not dfhack.screen.inGraphicsMode() then return end + if self:viewportChanged() then + self:reloadVisibleNotes() + end + dc:map(true) - -- local texpos = dfhack.textures.getTexposByHandle(green_pin[1]) - local texpos = textures.tp_green_pin(1) - local color, ch = COLOR_RED, 'X' - dc:pen({ch='X', fg=COLOR_GREEN, bg=COLOR_BLACK, tile=texpos}) + + local texpos = dfhack.textures.getTexposByHandle(green_pin[1]) + dc:pen({fg=COLOR_BLACK, bg=COLOR_LIGHTCYAN, tile=texpos}) + + for _, point in pairs(self.notes) do + dc + :seek(point.pos.x, point.pos.y) + :char('N') + end + + dc:map(false) +end + +function NotesOverlay:reloadVisibleNotes() + print('reloading notes') + self.notes = {} local viewport = guidm.Viewport.get() + self.viewport_pos = { + x=df.global.window_x, + y=df.global.window_y, + z=df.global.window_z + } for _, point in ipairs(df.global.plotinfo.waypoints.points) do if viewport:isVisible(point.pos) then local pos = viewport:tileToScreen(point.pos) - dc - :seek(pos.x, pos.y) - :tile() + table.insert(self.notes, { + name=point.name, + comment=point.comment, + pos=pos + }) end end - - dc:map(false) - end NoteManager = defclass(NoteManager, gui.ZScreen) @@ -71,6 +107,7 @@ NoteManager.ATTRS{ focus_path='hotspot/menu', hotspot_frame=DEFAULT_NIL, note=DEFAULT_NIL, + on_update=DEFAULT_NIL, } function NoteManager:init() @@ -129,17 +166,12 @@ function NoteManager:init() on_activate=function() self:createNote() end, enabled=function() return #self.subviews.name:getText() > 0 end, }, - widgets.TextButton{ - view_id='cancel', - frame={h=1}, - label='Cancel', - key='LEAVESCREEN' - }, edit_mode and widgets.TextButton{ view_id='delete', frame={h=1}, label='Delete', key='CUSTOM_ALT_D', + on_activate=function() self:deleteNote() end, } or nil, } } @@ -178,6 +210,11 @@ function NoteManager:createNote() pos=xyz2pos(x, y, z) }) waypoints.next_point_id = waypoints.next_point_id + 1 + + if self.on_update then + self.on_update() + end + self:dismiss() end @@ -198,6 +235,29 @@ function NoteManager:saveNote() self.note.name = name self.note.comment = comment + if self.on_update then + self.on_update() + end + + self:dismiss() +end + +function NoteManager:deleteNote() + if self.note == nil then + return + end + + for ind, note in pairs(df.global.plotinfo.waypoints.points) do + if note == self.note then + df.global.plotinfo.waypoints.points:erase(ind) + break + end + end + + if self.on_update then + self.on_update() + end + self:dismiss() end From 50b53bc3ee140e0301ac42f9de223137648b7a4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 3 Sep 2024 09:03:03 +0200 Subject: [PATCH 042/811] Allow to iterate map notes on same tile by click --- notes.lua | 71 +++++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 53 insertions(+), 18 deletions(-) diff --git a/notes.lua b/notes.lua index 9b65896692..025cc2c816 100644 --- a/notes.lua +++ b/notes.lua @@ -20,6 +20,8 @@ NotesOverlay.ATTRS{ function NotesOverlay:init() self.notes = {} + self.note_manager = nil + self.last_click_pos = {} self:reloadVisibleNotes() end @@ -27,27 +29,58 @@ function NotesOverlay:overlay_onupdate() self:reloadVisibleNotes() end -function NotesOverlay:overlay_trigger() - print('called') - -- self:reloadVisibleNotes() +function NotesOverlay:overlay_trigger(args) + return self:showNoteManager() end - function NotesOverlay:onInput(keys) if keys._MOUSE_L then local pos = dfhack.gui.getMousePos() - local notes = df.global.plotinfo.waypoints.points - for _, note in pairs(notes) do - if same_xyz(note.pos, pos) then - NoteManager{ - note=note, - on_update=function() self:reloadVisibleNotes() end - }:show() + local note = self:clickedNote(pos) + if note ~= nil then + self:showNoteManager(note) + end + end +end + +function NotesOverlay:clickedNote(click_pos) + local pos_curr_note = same_xyz(self.last_click_pos, click_pos) + and self.note_manager + and self.note_manager.note + or nil + + self.last_click_pos = click_pos + + local notes = df.global.plotinfo.waypoints.points + + local last_note_on_pos = nil + local first_note_on_pos = nil + for _, note in pairs(notes) do + if same_xyz(note.pos, click_pos) then + if last_note_on_pos == pos_curr_note then + return note end + + first_note_on_pos = first_note_on_pos or note + last_note_on_pos = note end + end + return first_note_on_pos +end + +function NotesOverlay:showNoteManager(note) + if self.note_manager ~= nil then + self.note_manager:dismiss() end + + self.note_manager = NoteManager{ + note=note, + on_update=function() self:reloadVisibleNotes() end + } + + return self.note_manager:show() end function NotesOverlay:viewportChanged() @@ -70,9 +103,9 @@ function NotesOverlay:onRenderFrame(dc) local texpos = dfhack.textures.getTexposByHandle(green_pin[1]) dc:pen({fg=COLOR_BLACK, bg=COLOR_LIGHTCYAN, tile=texpos}) - for _, point in pairs(self.notes) do + for _, note in pairs(self.notes) do dc - :seek(point.pos.x, point.pos.y) + :seek(note.screen_pos.x, note.screen_pos.y) :char('N') end @@ -80,7 +113,6 @@ function NotesOverlay:onRenderFrame(dc) end function NotesOverlay:reloadVisibleNotes() - print('reloading notes') self.notes = {} local viewport = guidm.Viewport.get() @@ -94,9 +126,8 @@ function NotesOverlay:reloadVisibleNotes() if viewport:isVisible(point.pos) then local pos = viewport:tileToScreen(point.pos) table.insert(self.notes, { - name=point.name, - comment=point.comment, - pos=pos + point=point, + screen_pos=pos }) end end @@ -261,6 +292,10 @@ function NoteManager:deleteNote() self:dismiss() end +function NoteManager:onDismiss() + self.note = nil +end + -- register widgets OVERLAY_WIDGETS = { map_notes=NotesOverlay @@ -278,7 +313,7 @@ local function main(args) return end - return NoteManager{}:show() + return dfhack.internal.runCommand('overlay trigger notes.map_notes') end end From b91876095d0cbd9910878043ad2a3c45a99dac63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 3 Sep 2024 18:55:39 +0200 Subject: [PATCH 043/811] Make text_editor mouse focus works only when click on its area --- internal/journal/text_editor.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 7b5a422c36..71521d5801 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -234,7 +234,7 @@ function TextEditor:onInput(keys) return self.subviews.scrollbar:onInput(keys) end - if keys._MOUSE_L then + if keys._MOUSE_L and self:getMousePos() then self:setFocus(true) end From 5109ce562c00554e1289686a252b790d51b5c579 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 3 Sep 2024 19:18:09 +0200 Subject: [PATCH 044/811] Polishing notes implementation --- notes.lua | 71 +++++++++++++++++++++++++++---------------------------- 1 file changed, 35 insertions(+), 36 deletions(-) diff --git a/notes.lua b/notes.lua index 025cc2c816..feb710eba5 100644 --- a/notes.lua +++ b/notes.lua @@ -15,9 +15,11 @@ NotesOverlay.ATTRS{ viewscreens='dwarfmode', default_enabled=true, overlay_onupdate_max_freq_seconds=30, - fullscreen=true, } +local waypoints = df.global.plotinfo.waypoints +local map_notes = df.global.plotinfo.waypoints.points + function NotesOverlay:init() self.notes = {} self.note_manager = nil @@ -35,11 +37,14 @@ end function NotesOverlay:onInput(keys) if keys._MOUSE_L then - local pos = dfhack.gui.getMousePos() + local top_most_screen = dfhack.gui.getDFViewscreen(true) + if dfhack.gui.matchFocusString('dwarfmode/Default', top_most_screen) then + local pos = dfhack.gui.getMousePos() - local note = self:clickedNote(pos) - if note ~= nil then - self:showNoteManager(note) + local note = self:clickedNote(pos) + if note ~= nil then + self:showNoteManager(note) + end end end end @@ -52,11 +57,9 @@ function NotesOverlay:clickedNote(click_pos) self.last_click_pos = click_pos - local notes = df.global.plotinfo.waypoints.points - local last_note_on_pos = nil local first_note_on_pos = nil - for _, note in pairs(notes) do + for _, note in ipairs(map_notes) do if same_xyz(note.pos, click_pos) then if last_note_on_pos == pos_curr_note then return note @@ -122,7 +125,7 @@ function NotesOverlay:reloadVisibleNotes() z=df.global.window_z } - for _, point in ipairs(df.global.plotinfo.waypoints.points) do + for _, point in ipairs(map_notes) do if viewport:isVisible(point.pos) then local pos = viewport:tileToScreen(point.pos) table.insert(self.notes, { @@ -135,8 +138,7 @@ end NoteManager = defclass(NoteManager, gui.ZScreen) NoteManager.ATTRS{ - focus_path='hotspot/menu', - hotspot_frame=DEFAULT_NIL, + focus_path='notes/note-manager', note=DEFAULT_NIL, on_update=DEFAULT_NIL, } @@ -148,7 +150,6 @@ function NoteManager:init() widgets.Window{ frame={w=35,h=20}, frame_inset={t=1}, - autoarrange_subviews=false, subviews={ widgets.HotkeyLabel { key='CUSTOM_ALT_N', @@ -158,50 +159,51 @@ function NoteManager:init() }, text_editor.TextEditor{ view_id='name', - focus_path='notes/name', frame={t=1,h=3}, frame_style=gui.FRAME_INTERIOR, - frame_style_b=nil, init_text=self.note and self.note.name or '' }, widgets.HotkeyLabel { key='CUSTOM_ALT_C', label='Comment', - frame={t=4}, + frame={t=5}, on_activate=function() self.subviews.comment:setFocus(true) end, }, text_editor.TextEditor{ view_id='comment', - frame={t=5,b=3}, - focus_path='notes/comment', + frame={t=6,b=3}, frame_style=gui.FRAME_INTERIOR, init_text=self.note and self.note.comment or '' }, widgets.Panel{ view_id='buttons', - frame={b=0,h=3}, + frame={b=0,h=2}, autoarrange_subviews=true, subviews={ - edit_mode and widgets.TextButton{ + widgets.HotkeyLabel{ view_id='Save', frame={h=1}, label='Save', key='CUSTOM_ALT_S', + visible=edit_mode, on_activate=function() self:saveNote() end, enabled=function() return #self.subviews.name:getText() > 0 end, - } or widgets.TextButton{ + }, + widgets.HotkeyLabel{ view_id='Create', frame={h=1}, label='Create', key='CUSTOM_ALT_S', + visible=not edit_mode, on_activate=function() self:createNote() end, enabled=function() return #self.subviews.name:getText() > 0 end, }, - edit_mode and widgets.TextButton{ + widgets.HotkeyLabel{ view_id='delete', frame={h=1}, label='Delete', key='CUSTOM_ALT_D', + visible=edit_mode, on_activate=function() self:deleteNote() end, } or nil, } @@ -212,8 +214,8 @@ function NoteManager:init() end function NoteManager:createNote() - local x, y, z = pos2xyz(df.global.cursor) - if x == nil then + local cursor_pos = guidm.getCursorPos() + if cursor_pos == nil then print('Enable keyboard cursor to add a note.') return end @@ -222,14 +224,12 @@ function NoteManager:createNote() local comment = self.subviews.comment:getText() if #name == 0 then - print('Note need at least a name') + dfhack.printerr('Note need at least a name') return end - local waypoints = df.global.plotinfo.waypoints - local notes = df.global.plotinfo.waypoints.points - notes:insert("#", { + map_notes:insert("#", { new=true, id = waypoints.next_point_id, @@ -238,7 +238,7 @@ function NoteManager:createNote() bg_color=0, name=name, comment=comment, - pos=xyz2pos(x, y, z) + pos=cursor_pos }) waypoints.next_point_id = waypoints.next_point_id + 1 @@ -258,11 +258,10 @@ function NoteManager:saveNote() local comment = self.subviews.comment:getText() if #name == 0 then - print('Note need at least a name') + dfhack.printerr('Note need at least a name') return end - local notes = df.global.plotinfo.waypoints.points self.note.name = name self.note.comment = comment @@ -278,9 +277,9 @@ function NoteManager:deleteNote() return end - for ind, note in pairs(df.global.plotinfo.waypoints.points) do - if note == self.note then - df.global.plotinfo.waypoints.points:erase(ind) + for ind, note in pairs(map_notes) do + if note.id == self.note.id then + map_notes:erase(ind) break end end @@ -307,9 +306,9 @@ local function main(args) end if args[1] == 'add' then - local x = pos2xyz(df.global.cursor) - if x == nil then - print('Enable keyboard cursor to add a note.') + local cursor_pos = guidm.getCursorPos() + if cursor_pos == nil then + dfhack.printerr('Enable keyboard cursor to add a note.') return end From bedd76d6dad7b6182b9c65e43f96c419fc17e844 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 3 Sep 2024 19:59:18 +0200 Subject: [PATCH 045/811] Optimize notes to only scan notes on screen when clicked --- notes.lua | 37 +++++++++++++++++++------------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/notes.lua b/notes.lua index feb710eba5..2a0e3f95fa 100644 --- a/notes.lua +++ b/notes.lua @@ -18,7 +18,7 @@ NotesOverlay.ATTRS{ } local waypoints = df.global.plotinfo.waypoints -local map_notes = df.global.plotinfo.waypoints.points +local map_points = df.global.plotinfo.waypoints.points function NotesOverlay:init() self.notes = {} @@ -59,9 +59,11 @@ function NotesOverlay:clickedNote(click_pos) local last_note_on_pos = nil local first_note_on_pos = nil - for _, note in ipairs(map_notes) do - if same_xyz(note.pos, click_pos) then - if last_note_on_pos == pos_curr_note then + for _, note in ipairs(self.notes) do + if same_xyz(note.point.pos, click_pos) then + if (last_note_on_pos and pos_curr_note + and last_note_on_pos.point.id == pos_curr_note.point.id + ) then return note end @@ -125,12 +127,12 @@ function NotesOverlay:reloadVisibleNotes() z=df.global.window_z } - for _, point in ipairs(map_notes) do - if viewport:isVisible(point.pos) then - local pos = viewport:tileToScreen(point.pos) + for _, map_point in ipairs(map_points) do + if viewport:isVisible(map_point.pos) then + local screen_pos = viewport:tileToScreen(map_point.pos) table.insert(self.notes, { - point=point, - screen_pos=pos + point=map_point, + screen_pos=screen_pos }) end end @@ -161,7 +163,7 @@ function NoteManager:init() view_id='name', frame={t=1,h=3}, frame_style=gui.FRAME_INTERIOR, - init_text=self.note and self.note.name or '' + init_text=self.note and self.note.point.name or '' }, widgets.HotkeyLabel { key='CUSTOM_ALT_C', @@ -173,7 +175,7 @@ function NoteManager:init() view_id='comment', frame={t=6,b=3}, frame_style=gui.FRAME_INTERIOR, - init_text=self.note and self.note.comment or '' + init_text=self.note and self.note.point.comment or '' }, widgets.Panel{ view_id='buttons', @@ -228,8 +230,7 @@ function NoteManager:createNote() return end - - map_notes:insert("#", { + map_points:insert("#", { new=true, id = waypoints.next_point_id, @@ -262,8 +263,8 @@ function NoteManager:saveNote() return end - self.note.name = name - self.note.comment = comment + self.note.point.name = name + self.note.point.comment = comment if self.on_update then self.on_update() @@ -277,9 +278,9 @@ function NoteManager:deleteNote() return end - for ind, note in pairs(map_notes) do - if note.id == self.note.id then - map_notes:erase(ind) + for ind, map_point in pairs(map_points) do + if map_point.id == self.note.point.id then + map_points:erase(ind) break end end From 7a7c6eb01c62be5c52988a4e124ed0236087605f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 3 Sep 2024 20:41:55 +0200 Subject: [PATCH 046/811] Make notes modal cursor starts at the begining of the edited note text --- notes.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/notes.lua b/notes.lua index 2a0e3f95fa..c1705e915a 100644 --- a/notes.lua +++ b/notes.lua @@ -163,7 +163,8 @@ function NoteManager:init() view_id='name', frame={t=1,h=3}, frame_style=gui.FRAME_INTERIOR, - init_text=self.note and self.note.point.name or '' + init_text=self.note and self.note.point.name or '', + init_cursor=1 }, widgets.HotkeyLabel { key='CUSTOM_ALT_C', @@ -175,7 +176,8 @@ function NoteManager:init() view_id='comment', frame={t=6,b=3}, frame_style=gui.FRAME_INTERIOR, - init_text=self.note and self.note.point.comment or '' + init_text=self.note and self.note.point.comment or '', + init_cursor=1 }, widgets.Panel{ view_id='buttons', From 97221a131f262560a37ccfa7fcf2ee4df41761da Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 3 Sep 2024 12:50:12 -0700 Subject: [PATCH 047/811] style --- internal/advtools/party.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/advtools/party.lua b/internal/advtools/party.lua index 241edc4782..4ef9f7cbf9 100644 --- a/internal/advtools/party.lua +++ b/internal/advtools/party.lua @@ -1,7 +1,7 @@ --@ module=true -local dialogs = require 'gui.dialogs' -local utils = require 'utils' +local dialogs = require('gui.dialogs') +local utils = require('utils') local makeown = reqscript('makeown') @@ -40,7 +40,7 @@ local function showExtraPartyPrompt() table.insert(choices, {text=name, nemesis=nemesis, search_key=dfhack.toSearchNormalized(name)}) ::continue:: end - dialogs.showListPrompt('party', "Select someone to add to your \"Core Party\" (able to assume control, able to unretire):", COLOR_WHITE, + dialogs.showListPrompt('party', 'Select someone to add to your "Core Party" (able to assume control, able to unretire):', COLOR_WHITE, choices, function(id, choice) addToCoreParty(choice.nemesis) end, nil, nil, true) From 45e3dbd4200c6b7c6a2f3dc2d2f76f2f3ab1eaef Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 3 Sep 2024 12:51:44 -0700 Subject: [PATCH 048/811] add info on bridging two landmasses --- changelog.txt | 3 +++ docs/gui/embark-anywhere.rst | 47 ++++++++++++++++++++++++++++++------ 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/changelog.txt b/changelog.txt index 28a17540e3..dc2e423dbb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -49,6 +49,9 @@ Template for new versions: - `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma - `position`: report current historical era (e.g., "Age of Myth") +## Documentation +- `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses + ## Removed # 50.13-r4 diff --git a/docs/gui/embark-anywhere.rst b/docs/gui/embark-anywhere.rst index 4583b04cd7..db5c06fc34 100644 --- a/docs/gui/embark-anywhere.rst +++ b/docs/gui/embark-anywhere.rst @@ -11,8 +11,13 @@ embark in an inaccessible location on top of a mountain range? Go for it! Want to try a brief existence in the middle of the ocean? Nobody can stop you! Want to tempt fate by embarking *inside of* a necromancer tower? !!FUN!! +If you are using this tool to create a fort that will bridge two disconnected +areas of land, see `So you want to bridge a gap?`_ below for tips and caveats. + Any and all consequences of embarking in strange locations are up to you to -handle (possibly with other `armok ` tools). +handle (possibly with other `armok ` tools). In particular, +embarking in inaccessible locations will prevent migrants, caravans, and +visitors from arriving. Usage ----- @@ -21,9 +26,37 @@ Usage gui/embark-anywhere -The command will only work when you are on the screen where you can choose your -embark site. The DFHack logo is not displayed on that screen since its default -position conflicts with the vanilla embark size selection panel. Remember that -you can bring up DFHack's `context menu ` with -:kbd:`Ctrl`:kbd:`Shift`:kbd:`C` or the -`in-game command launcher ` directly with the backtick key (\`). +The command will only work when you are on the screen where you can choose the +embark site for your fort. + +So you want to bridge a gap? +---------------------------- + +A popular use case for this tool is to create a fort (or a series of forts) that +bridges two disconnected landmasses so sites on the two landmasses can reach +each other (that is, they can send raiding parties and/or engage in trade). + +However, the way this works is not entirely intuitive. + +A single large embark is not necessarily going to functionally connect the two +shores so that armies can cross the gap. You could still choose to use this +approach to build a continuous constructed bridge in fort mode for later use as +an *adventurer* in adventure mode, but it will not be usable by the other +characters/armies in the world. + +The DF world map is divided into blocks of 16x16 tiles. When you are choosing +where to embark and you move the mouse so that your embark area "shadow" moves +over a little bit -- that's one "tile". An embark area can span block +boundaries, and there is no indication on the map where those boundaries are. + +The way DF determines world pathability is to check if the ground is continuous +**or** if the enclosing 16x16 block contains the upper left tile of a fort +embark area. + +In order for a connection to be formed for armies, one fort upper left corner +must exist in each 16x16 block that contains part of the gap. + +Therefore, the simplest solution for making a "bridge" that armies can use (but +walking adventurers cannot) is to make a 1x1 fort every 16 tiles across the +water gap, starting on land on one shore and finishing on land on the opposite +shore. That will ensure that every 16x16 block in the gap is covered by a fort. From fe1809fb5a0e074f1257a9bde5677889d3bdb309 Mon Sep 17 00:00:00 2001 From: dikbutdagrate <73856869+Tjudge1@users.noreply.github.com> Date: Tue, 3 Sep 2024 18:41:59 -0400 Subject: [PATCH 049/811] Update create-item.lua Refactored code, tested, works, etc. --- modtools/create-item.lua | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/modtools/create-item.lua b/modtools/create-item.lua index d05438c1e6..1615922efe 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -335,27 +335,18 @@ function hackWish(accessors, opts) until count end if not mattype or not itemtype then return end - if not typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and df.item_type.attrs[itemtype].is_stackable then - return createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, count) - end - if typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and df.item_type.attrs[itemtype].is_stackable then - return createItem({matindex, casteId}, {itemtype, itemsubtype}, quality, unit, description, count) + if df.item_type.attrs[itemtype].is_stackable then + local mat = typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and {matindex, casteId} or {mattype, matindex} + return createItem(mat, {itemtype, itemsubtype}, quality, unit, description, count) end local items = {} for _ = 1,count do if itemtype == df.item_type.CORPSEPIECE or itemtype == df.item_type.CORPSE then table.insert(items, createCorpsePiece(unit, bodypart, partlayerID, matindex, casteId, corpsepieceGeneric)) else - if typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] then - for - _,item in ipairs(createItem({matindex, casteId}, {itemtype, itemsubtype}, quality, unit, description, 1)) do - table.insert(items, item) - end - else - for - _,item in ipairs(createItem({mattype, matindex}, {itemtype, itemsubtype}, quality, unit, description, 1)) do - table.insert(items, item) - end + local mat = typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and {matindex, casteId} or {mattype, matindex} + for _,item in ipairs(createItem(mat, {itemtype, itemsubtype}, quality, unit, description, 1)) do + table.insert(items, item) end end end From 16298298bc7ec55c463169464146d70f9744768c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 22:47:45 +0000 Subject: [PATCH 050/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- modtools/create-item.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/modtools/create-item.lua b/modtools/create-item.lua index 1615922efe..95811af968 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -345,8 +345,8 @@ function hackWish(accessors, opts) table.insert(items, createCorpsePiece(unit, bodypart, partlayerID, matindex, casteId, corpsepieceGeneric)) else local mat = typesThatUseCreaturesExceptCorpses[df.item_type[itemtype]] and {matindex, casteId} or {mattype, matindex} - for _,item in ipairs(createItem(mat, {itemtype, itemsubtype}, quality, unit, description, 1)) do - table.insert(items, item) + for _,item in ipairs(createItem(mat, {itemtype, itemsubtype}, quality, unit, description, 1)) do + table.insert(items, item) end end end From 02e193318f3dfe1bac989e232c378b96e1cff272 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 4 Sep 2024 06:54:46 +0200 Subject: [PATCH 051/811] Improve new/edit note modal design --- notes.lua | 35 ++++++++++++++++++++++------------- 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/notes.lua b/notes.lua index c1705e915a..b06abcf04f 100644 --- a/notes.lua +++ b/notes.lua @@ -21,7 +21,7 @@ local waypoints = df.global.plotinfo.waypoints local map_points = df.global.plotinfo.waypoints.points function NotesOverlay:init() - self.notes = {} + self.visible_notes = {} self.note_manager = nil self.last_click_pos = {} self:reloadVisibleNotes() @@ -40,6 +40,9 @@ function NotesOverlay:onInput(keys) local top_most_screen = dfhack.gui.getDFViewscreen(true) if dfhack.gui.matchFocusString('dwarfmode/Default', top_most_screen) then local pos = dfhack.gui.getMousePos() + if pos == nil then + return false + end local note = self:clickedNote(pos) if note ~= nil then @@ -59,7 +62,7 @@ function NotesOverlay:clickedNote(click_pos) local last_note_on_pos = nil local first_note_on_pos = nil - for _, note in ipairs(self.notes) do + for _, note in ipairs(self.visible_notes) do if same_xyz(note.point.pos, click_pos) then if (last_note_on_pos and pos_curr_note and last_note_on_pos.point.id == pos_curr_note.point.id @@ -108,7 +111,7 @@ function NotesOverlay:onRenderFrame(dc) local texpos = dfhack.textures.getTexposByHandle(green_pin[1]) dc:pen({fg=COLOR_BLACK, bg=COLOR_LIGHTCYAN, tile=texpos}) - for _, note in pairs(self.notes) do + for _, note in pairs(self.visible_notes) do dc :seek(note.screen_pos.x, note.screen_pos.y) :char('N') @@ -118,7 +121,7 @@ function NotesOverlay:onRenderFrame(dc) end function NotesOverlay:reloadVisibleNotes() - self.notes = {} + self.visible_notes = {} local viewport = guidm.Viewport.get() self.viewport_pos = { @@ -130,7 +133,7 @@ function NotesOverlay:reloadVisibleNotes() for _, map_point in ipairs(map_points) do if viewport:isVisible(map_point.pos) then local screen_pos = viewport:tileToScreen(map_point.pos) - table.insert(self.notes, { + table.insert(self.visible_notes, { point=map_point, screen_pos=screen_pos }) @@ -152,11 +155,13 @@ function NoteManager:init() widgets.Window{ frame={w=35,h=20}, frame_inset={t=1}, + resizable=true, subviews={ widgets.HotkeyLabel { key='CUSTOM_ALT_N', label='Name', - frame={t=0}, + frame={l=0,t=0}, + auto_width=true, on_activate=function() self.subviews.name:setFocus(true) end, }, text_editor.TextEditor{ @@ -169,7 +174,8 @@ function NoteManager:init() widgets.HotkeyLabel { key='CUSTOM_ALT_C', label='Comment', - frame={t=5}, + frame={l=0,t=5}, + auto_width=true, on_activate=function() self.subviews.comment:setFocus(true) end, }, text_editor.TextEditor{ @@ -181,12 +187,13 @@ function NoteManager:init() }, widgets.Panel{ view_id='buttons', - frame={b=0,h=2}, - autoarrange_subviews=true, + frame={b=0,h=1}, + frame_inset={l=1,r=1}, subviews={ widgets.HotkeyLabel{ view_id='Save', - frame={h=1}, + frame={l=0,t=0,h=1}, + auto_width=true, label='Save', key='CUSTOM_ALT_S', visible=edit_mode, @@ -195,7 +202,8 @@ function NoteManager:init() }, widgets.HotkeyLabel{ view_id='Create', - frame={h=1}, + frame={l=0,t=0,h=1}, + auto_width=true, label='Create', key='CUSTOM_ALT_S', visible=not edit_mode, @@ -204,7 +212,8 @@ function NoteManager:init() }, widgets.HotkeyLabel{ view_id='delete', - frame={h=1}, + frame={r=0,t=0,h=1}, + auto_width=true, label='Delete', key='CUSTOM_ALT_D', visible=edit_mode, @@ -220,7 +229,7 @@ end function NoteManager:createNote() local cursor_pos = guidm.getCursorPos() if cursor_pos == nil then - print('Enable keyboard cursor to add a note.') + dfhack.printerr('Enable keyboard cursor to add a note.') return end From 37fcaa1073c51601fc81b1bf08bde7b9b71e1b03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 4 Sep 2024 07:04:34 +0200 Subject: [PATCH 052/811] Hide notes that have no "name" (most likely they are waypoints) --- notes.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/notes.lua b/notes.lua index b06abcf04f..90e50b8610 100644 --- a/notes.lua +++ b/notes.lua @@ -131,7 +131,9 @@ function NotesOverlay:reloadVisibleNotes() } for _, map_point in ipairs(map_points) do - if viewport:isVisible(map_point.pos) then + if (viewport:isVisible(map_point.pos) + and map_point.name ~= nil and #map_point.name > 0) + then local screen_pos = viewport:tileToScreen(map_point.pos) table.insert(self.visible_notes, { point=map_point, From 10d45177484e3abe727e0d2cc2412f41788bb801 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 3 Sep 2024 22:39:08 -0700 Subject: [PATCH 053/811] make deep-embark work in embarks with no wagon and no land --- changelog.txt | 2 + deep-embark.lua | 528 +++++++++++++++++++++++++----------------------- 2 files changed, 272 insertions(+), 258 deletions(-) diff --git a/changelog.txt b/changelog.txt index dc2e423dbb..ca19f88427 100644 --- a/changelog.txt +++ b/changelog.txt @@ -42,6 +42,8 @@ Template for new versions: - `timestream`: ensure child growth events (e.g. becoming an adult) are not skipped - `empty-bin`: ``--liquids`` option now correctly empties containers filled with LIQUID_MISC (like lye) - `gui/design`: don't overcount "affected tiles" for Line & Freeform drawing tools +- `deep-embark`: fix error when embarking where there is no land to stand on (e.g. when embarking in the ocean with `gui/embark-anywhere` +- `deep-embark`: fix failure to transport units and items when embarking where there is no room to spawn the starting wagon ## Misc Improvements - `gui/sitemap`: show whether a unit is friendly, hostile, or wild diff --git a/deep-embark.lua b/deep-embark.lua index 2e5f141283..1745774a9b 100644 --- a/deep-embark.lua +++ b/deep-embark.lua @@ -1,348 +1,360 @@ --@ module = true -local utils = require 'utils' +local dlg = require('gui.dialogs') +local utils = require('utils') function getFeatureID(cavernType) - local features = df.global.world.features - local map_features = features.map_features - if cavernType == 'CAVERN_1' then - for i, feature in ipairs(map_features) do - if feature._type == df.feature_init_subterranean_from_layerst - and feature.start_depth == 0 then - return features.feature_global_idx[i] - end - end - elseif cavernType == 'CAVERN_2' then - for i, feature in ipairs(map_features) do - if feature._type == df.feature_init_subterranean_from_layerst - and feature.start_depth == 1 then - return features.feature_global_idx[i] - end - end - elseif cavernType == 'CAVERN_3' then - for i, feature in ipairs(map_features) do - if feature._type == df.feature_init_subterranean_from_layerst - and feature.start_depth == 2 then - return features.feature_global_idx[i] - end - end - elseif cavernType == 'UNDERWORLD' then - for i, feature in ipairs(map_features) do - if feature._type == df.feature_init_underworld_from_layerst - and feature.start_depth == 4 then - return features.feature_global_idx[i] - end + local features = df.global.world.features + local map_features = features.map_features + if cavernType == 'CAVERN_1' then + for i, feature in ipairs(map_features) do + if feature._type == df.feature_init_subterranean_from_layerst and feature.start_depth == 0 then + return features.feature_global_idx[i] + end + end + elseif cavernType == 'CAVERN_2' then + for i, feature in ipairs(map_features) do + if feature._type == df.feature_init_subterranean_from_layerst and feature.start_depth == 1 then + return features.feature_global_idx[i] + end + end + elseif cavernType == 'CAVERN_3' then + for i, feature in ipairs(map_features) do + if feature._type == df.feature_init_subterranean_from_layerst and feature.start_depth == 2 then + return features.feature_global_idx[i] + end + end + elseif cavernType == 'UNDERWORLD' then + for i, feature in ipairs(map_features) do + if feature._type == df.feature_init_underworld_from_layerst and feature.start_depth == 4 then + return features.feature_global_idx[i] + end + end end - end end function getFeatureBlocks(featureID) - local featureBlocks = {} --as:number[] - for i,block in ipairs(df.global.world.map.map_blocks) do - if block.global_feature == featureID and block.local_feature == -1 then - table.insert(featureBlocks, i) + local featureBlocks = {} --as:number[] + for i, block in ipairs(df.global.world.map.map_blocks) do + if block.global_feature == featureID and block.local_feature == -1 then + table.insert(featureBlocks, i) + end end - end - return featureBlocks + return featureBlocks end function isValidTiletype(tiletype) - local tt = df.tiletype[tiletype] - local tiletypeAttrs = df.tiletype.attrs[tt] - local material = tiletypeAttrs.material - local forbiddenMaterials = { - df.tiletype_material.TREE, -- so as not to embark stranded on top of a tree - df.tiletype_material.MUSHROOM, - df.tiletype_material.FIRE, - df.tiletype_material.CAMPFIRE - } - for _,forbidden in ipairs(forbiddenMaterials) do - if material == forbidden then - return false - end - end - local shapeAttrs = df.tiletype_shape.attrs[tiletypeAttrs.shape] - return shapeAttrs.walkable + local tt = df.tiletype[tiletype] + local tiletypeAttrs = df.tiletype.attrs[tt] + local material = tiletypeAttrs.material + local forbiddenMaterials = utils.invert{ + df.tiletype_material.TREE, -- so as not to embark stranded on top of a tree + df.tiletype_material.MUSHROOM, + df.tiletype_material.FIRE, + df.tiletype_material.CAMPFIRE + } + if forbiddenMaterials[material] then return false end + local shapeAttrs = df.tiletype_shape.attrs[tiletypeAttrs.shape] + return shapeAttrs.walkable end function getValidEmbarkTiles(block) - local validTiles = {} --as:{_type:table,x:number,y:number,z:number}[] - for xi = 0,15 do - for yi = 0,15 do - if block.designation[xi][yi].flow_size == 0 - and isValidTiletype(block.tiletype[xi][yi]) then - table.insert(validTiles, {x = block.map_pos.x + xi, y = block.map_pos.y + yi, z = block.map_pos.z}) - end + local validTiles = {} --as:{_type:table,x:number,y:number,z:number}[] + for xi = 0, 15 do + for yi = 0, 15 do + if block.designation[xi][yi].flow_size == 0 + and isValidTiletype(block.tiletype[xi][yi]) + then + table.insert(validTiles, { x = block.map_pos.x + xi, y = block.map_pos.y + yi, z = block.map_pos.z }) + end + end end - end - return validTiles + return validTiles end function blockGlowingBarrierAnnouncements(recenter) --- temporarily disables the "glowing barrier has disappeared" announcement --- announcement settings are restored after 1 tick --- setting recenter to true enables recentering of game view to the announcement position - local announcementFlags = df.global.d_init.announcements.flags.ENDGAME_EVENT_1 -- glowing barrier disappearance announcement - local oldFlags = df.global.d_init.announcements.flags.ENDGAME_EVENT_1:new() -- backup announcement settings - announcementFlags.DO_MEGA = false - announcementFlags.PAUSE = false - announcementFlags.RECENTER = recenter and true or false - announcementFlags.A_DISPLAY = false - announcementFlags.D_DISPLAY = recenter and true or false -- an actual announcement is required for recentering to occur - dfhack.timeout(1,'ticks', function() -- barrier disappears after 1 tick - announcementFlags:assign(oldFlags) -- restore announcement settings - if recenter then - -- Remove glowing barrier notifications: - local status = df.global.world.status - local announcements = status.announcements - for i = #announcements-1, 0, -1 do - if string.find(announcements[i].text,"glowing barrier has disappeared") then - announcements:erase(i) - break - end - end - local reports = status.reports - for i = #reports-1, 0, -1 do - if string.find(reports[i].text,"glowing barrier has disappeared") then - reports:erase(i) - break + -- temporarily disables the "glowing barrier has disappeared" announcement + -- announcement settings are restored after 1 tick + -- setting recenter to true enables recentering of game view to the announcement position + -- glowing barrier disappearance announcement + local announcementFlags = df.global.d_init.announcements.flags.ENDGAME_EVENT_1 + local oldFlags = df.global.d_init.announcements.flags.ENDGAME_EVENT_1:new() -- backup announcement settings + announcementFlags.DO_MEGA = false + announcementFlags.PAUSE = false + announcementFlags.RECENTER = recenter and true or false + announcementFlags.A_DISPLAY = false + announcementFlags.D_DISPLAY = recenter and true or false -- an actual announcement is required for recentering to occur + dfhack.timeout(1, 'ticks', function() -- barrier disappears after 1 tick + announcementFlags:assign(oldFlags) -- restore announcement settings + if recenter then + -- Remove glowing barrier notifications: + local status = df.global.world.status + local announcements = status.announcements + for i = #announcements - 1, 0, -1 do + if string.find(announcements[i].text, "glowing barrier has disappeared") then + announcements:erase(i) + break + end + end + local reports = status.reports + for i = #reports - 1, 0, -1 do + if string.find(reports[i].text, "glowing barrier has disappeared") then + reports:erase(i) + break + end + end + status.display_timer = 0 -- to avoid displaying other announcements end - end - status.display_timer = 0 -- to avoid displaying other announcements - end - end) + end) end function reveal(pos) --- creates an unbound glowing barrier at the target location --- so as to trigger tile revelation when it disappears 1 tick later (fortress mode only) --- should be run in conjunction with blockGlowingBarrierAnnouncements() - local x,y,z = pos2xyz(pos) - local block = dfhack.maps.getTileBlock(x,y,z) - local tiletype = block.tiletype[x%16][y%16] - if tiletype ~= df.tiletype.GlowingBarrier then -- to avoid multiple instances - block.tiletype[x%16][y%16] = df.tiletype.GlowingBarrier + -- creates an unbound glowing barrier at the target location + -- so as to trigger tile revelation when it disappears 1 tick later (fortress mode only) + -- should be run in conjunction with blockGlowingBarrierAnnouncements() + local x, y, z = pos2xyz(pos) + local block = dfhack.maps.getTileBlock(x, y, z) + local tiletype = block.tiletype[x % 16][y % 16] + if tiletype == df.tiletype.GlowingBarrier then -- to avoid multiple instances + return + end + block.tiletype[x % 16][y % 16] = df.tiletype.GlowingBarrier local barriers = df.global.world.event.glowing_barriers local barrier = df.glowing_barrier:new() - barrier.buildings:insert('#',-1) -- being unbound to a building makes the barrier disappear immediately + barrier.buildings:insert('#', -1) -- being unbound to a building makes the barrier disappear immediately barrier.pos:assign(pos) - barriers:insert('#',barrier) + barriers:insert('#', barrier) local hfs = df.glowing_barrier:new() - hfs.triggered = 1 -- this prevents HFS events (which can otherwise be triggered by the barrier disappearing) - barriers:insert('#',hfs) - dfhack.timeout(1,'ticks', function() -- barrier tiletype disappears after 1 tick - block.tiletype[x%16][y%16] = tiletype -- restore old tiletype - barriers:erase(#barriers-1) -- remove hfs blocker - barriers:erase(#barriers-1) -- remove revelation barrier + hfs.triggered = 1 -- this prevents HFS events (which can otherwise be triggered by the barrier disappearing) + barriers:insert('#', hfs) + dfhack.timeout(1, 'ticks', function() -- barrier tiletype disappears after 1 tick + block.tiletype[x % 16][y % 16] = tiletype -- restore old tiletype + barriers:erase(#barriers - 1) -- remove hfs blocker + barriers:erase(#barriers - 1) -- remove revelation barrier end) - end end function moveEmbarkStuff(selectedBlock, embarkTiles) - local spawnPosCentre - for _, hotkey in ipairs(df.global.plotinfo.main.hotkeys) do - if hotkey.name == "Wagon arrival location" then -- the preset hotkey is centred around the spawn point - spawnPosCentre = xyz2pos(hotkey.x, hotkey.y, hotkey.z) - hotkey:assign(embarkTiles[math.random(1, #embarkTiles)]) -- set the hotkey to the new spawn point - break + local spawnPosCentre + for _, hotkey in ipairs(df.global.plotinfo.main.hotkeys) do + if hotkey.cmd == df.ui_hotkey.T_cmd.Zoom then -- the preset hotkey is centred around the spawn point + spawnPosCentre = xyz2pos(hotkey.x, hotkey.y, hotkey.z) + hotkey:assign(embarkTiles[math.random(1, #embarkTiles)]) -- set the hotkey to the new spawn point + break + end + end + + if not spawnPosCentre then -- no place for the wagon; use the position of the first unit + spawnPosCentre = xyz2pos(dfhack.units.getPosition(dfhack.units.getCitizens()[1])) end - end --- only target things within this zone to help avoid teleporting non-embark stuff: --- the following values might need to be modified - local x1 = spawnPosCentre.x - 15 - local x2 = spawnPosCentre.x + 15 - local y1 = spawnPosCentre.y - 15 - local y2 = spawnPosCentre.y + 15 - local z1 = spawnPosCentre.z - 3 -- units can be spread across multiple z-levels when embarking on a mountain - local z2 = spawnPosCentre.z + 3 + -- only target things within this zone to help avoid teleporting non-embark stuff: + -- the following values might need to be modified + local x1 = spawnPosCentre.x - 15 + local x2 = spawnPosCentre.x + 15 + local y1 = spawnPosCentre.y - 15 + local y2 = spawnPosCentre.y + 15 + local z1 = spawnPosCentre.z - 3 -- units can be spread across multiple z-levels when embarking on a mountain + local z2 = spawnPosCentre.z + 3 --- Move citizens and pets: - local unitsAtSpawn = dfhack.units.getUnitsInBox(x1,y1,z1,x2,y2,z2) - local movedUnit = false - for i, unit in ipairs(unitsAtSpawn) do - if unit.civ_id == df.global.plotinfo.civ_id and not unit.flags1.inactive and not unit.flags2.killed then - local pos = embarkTiles[math.random(1, #embarkTiles)] - dfhack.units.teleport(unit, pos) - reveal(pos) - movedUnit = true + -- Move citizens and pets: + local unitsAtSpawn = dfhack.units.getUnitsInBox(x1, y1, z1, x2, y2, z2) + local movedUnit = false + for i, unit in ipairs(unitsAtSpawn) do + if unit.civ_id == df.global.plotinfo.civ_id and not unit.flags1.inactive and not unit.flags2.killed then + local pos = embarkTiles[math.random(1, #embarkTiles)] + dfhack.units.teleport(unit, pos) + reveal(pos) + movedUnit = true + end + end + if movedUnit then + blockGlowingBarrierAnnouncements(true) -- this is separate from the reveal() function as it only needs to be called once per tick, regardless of how many times reveal() has been run end - end - if movedUnit then - blockGlowingBarrierAnnouncements(true) -- this is separate from the reveal() function as it only needs to be called once per tick, regardless of how many times reveal() has been run - end --- Move wagon contents: - local wagonFound = false - for _, wagon in ipairs(df.global.world.buildings.other.WAGON) do --as:df.building_wagonst - if wagon.age == 0 then -- just in case there's an older wagon present for some reason - local contained = wagon.contained_items - for i = #contained-1, 0, -1 do - if contained[i].use_mode == df.building_item_role_type.TEMP then -- actual contents (as opposed to building components) - local item = contained[i].item --- dfhack.items.moveToGround() does not handle items within buildings, so do this manually: - contained:erase(i) - for k = #item.general_refs-1, 0, -1 do - if item.general_refs[k]._type == df.general_ref_building_holderst then - item.general_refs:erase(k) + -- Move wagon contents: + local wagonFound = false + for _, wagon in ipairs(df.global.world.buildings.other.WAGON) do --as:df.building_wagonst + if wagon.age == 0 then -- just in case there's an older wagon present for some reason + local contained = wagon.contained_items + for i = #contained - 1, 0, -1 do + if contained[i].use_mode == df.building_item_role_type.TEMP then -- actual contents (as opposed to building components) + local item = contained[i].item + -- dfhack.items.moveToGround() does not handle items within buildings, so do this manually: + contained:erase(i) + for k = #item.general_refs - 1, 0, -1 do + if item.general_refs[k]._type == df.general_ref_building_holderst then + item.general_refs:erase(k) + end + end + item.flags.in_building = false + item.flags.on_ground = true + local pos = embarkTiles[math.random(1, #embarkTiles)] + item.pos:assign(pos) + selectedBlock.items:insert('#', item.id) + selectedBlock.occupancy[pos.x % 16][pos.y % 16].item = true + end end - end - item.flags.in_building = false - item.flags.on_ground = true - local pos = embarkTiles[math.random(1, #embarkTiles)] - item.pos:assign(pos) - selectedBlock.items:insert('#', item.id) - selectedBlock.occupancy[pos.x%16][pos.y%16].item = true + dfhack.buildings.deconstruct(wagon) + wagon.flags.almost_deleted = true -- wagon vanishes a tick later + wagonFound = true + break end - end - dfhack.buildings.deconstruct(wagon) - wagon.flags.almost_deleted = true -- wagon vanishes a tick later - wagonFound = true - break end - end --- Move items scattered around the spawn point if there's no wagon: - if not wagonFound then - for _, item in ipairs(df.global.world.items.other.IN_PLAY) do - local flags = item.flags - if item.age == 0 -- embark equipment consists of newly created items - and item.pos.x >= x1 and item.pos.x <= x2 - and item.pos.y >= y1 and item.pos.y <= y2 - and item.pos.z >= z1 and item.pos.z <= z2 - and flags.on_ground - and not flags.in_inventory - and not flags.in_building - and not flags.construction - and not flags.spider_web - and not flags.encased then - dfhack.items.moveToGround(item, embarkTiles[math.random(1, #embarkTiles)]) - end + -- Move items scattered around the spawn point if there's no wagon: + if not wagonFound then + for _, item in ipairs(df.global.world.items.other.IN_PLAY) do + local flags = item.flags + local item_pos = xyz2pos(dfhack.items.getPosition(item)) + -- items spawned into mid-air incorrectly have the `in_job` flag set + if flags.in_job then + local job_ref = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + if job_ref then + dfhack.job.removeJob(job_ref.data.job) + end + flags.in_job = false + end + if item.age == 0 -- embark equipment consists of newly created items + and item_pos.x >= x1 and item_pos.x <= x2 + and item_pos.y >= y1 and item_pos.y <= y2 + and item_pos.z >= z1 and item_pos.z <= z2 + and not flags.in_inventory + and not flags.in_building + and not flags.construction + and not flags.spider_web + and not flags.encased + then + dfhack.items.moveToGround(item, embarkTiles[math.random(1, #embarkTiles)]) + end + end end - end + + dlg.showMessage('deep-embark', 'Please unpause to zoom to your deep embark.', COLOR_WHITE) end function deepEmbark(cavernType, blockDemons) - if not cavernType then - qerror('Cavern type not specified!') - end + if not cavernType then + qerror('Cavern type not specified!') + end - local cavernBlocks = getFeatureBlocks(getFeatureID(cavernType)) - if #cavernBlocks == 0 then - qerror(cavernType .. " not found!") - end + local cavernBlocks = getFeatureBlocks(getFeatureID(cavernType)) + if #cavernBlocks == 0 then + qerror(cavernType .. " not found!") + end - local moved = false - for n = 1, #cavernBlocks do - local i = math.random(1, #cavernBlocks) - local selectedBlock = df.global.world.map.map_blocks[cavernBlocks[i]] - local embarkTiles = getValidEmbarkTiles(selectedBlock) - if #embarkTiles >= 20 then -- value chosen arbitrarily; might want to increase/decrease (determines how cramped the embark spot is allowed to be) - moveEmbarkStuff(selectedBlock, embarkTiles) - moved = true - break + local moved = false + for n = 1, #cavernBlocks do + local i = math.random(1, #cavernBlocks) + local selectedBlock = df.global.world.map.map_blocks[cavernBlocks[i]] + local embarkTiles = getValidEmbarkTiles(selectedBlock) + if #embarkTiles >= 20 then -- value chosen arbitrarily; might want to increase/decrease (determines how cramped the embark spot is allowed to be) + moveEmbarkStuff(selectedBlock, embarkTiles) + moved = true + break + end + table.remove(cavernBlocks, i) + end + if not moved then + qerror('Insufficient space at ' .. cavernType) end - table.remove(cavernBlocks, i) - end - if not moved then - qerror('Insufficient space at ' .. cavernType) - end - if blockDemons then - disableSpireDemons() - end + if blockDemons then + disableSpireDemons() + end end function disableSpireDemons() --- marks underworld spires on the map as having been breached already, preventing HFS events - for _, spire in ipairs(df.global.world.event.deep_vein_hollows) do - spire.triggered = true - end + -- marks underworld spires on the map as having been breached already, preventing HFS events + for _, spire in ipairs(df.global.world.event.deep_vein_hollows) do + spire.triggered = true + end end function inEmbarkMode() - if df.global.gametype ~= df.game_type.DWARF_MAIN then -- is always set at fortress mode setup - return false - end - local embarkViewScreens = { - df.viewscreen_adopt_regionst, -- onLoad.init kicks in early; this is the viewscreen present at this stage (the 'loading world' viewscreen is also present at adventure mode setup and legends mode, hence the game_type check above) - df.viewscreen_choose_start_sitest, - df.viewscreen_setupdwarfgamest - } - local view = dfhack.gui.getCurViewscreen() - for _, valid in ipairs(embarkViewScreens) do - if view._type == valid or view.parent._type == valid and view._type ~= df.viewscreen_textviewerst then -- df.viewscreen_textviewerst is present right after embarking (displays the embark message) and has .parent._type == df.viewscreen_setupdwarfgamest - return true + if df.global.gametype ~= df.game_type.DWARF_MAIN then -- is always set at fortress mode setup + return false + end + local embarkViewScreens = { + df.viewscreen_adopt_regionst, -- onLoad.init kicks in early; this is the viewscreen present at this stage (the 'loading world' viewscreen is also present at adventure mode setup and legends mode, hence the game_type check above) + df.viewscreen_choose_start_sitest, + df.viewscreen_setupdwarfgamest + } + local view = dfhack.gui.getCurViewscreen() + for _, valid in ipairs(embarkViewScreens) do + if view._type == valid then + return true + end end - end - return false + return false end local validArgs = utils.invert({ - 'depth', - 'atReclaim', - 'blockDemons', - 'clear', - 'help' + 'depth', + 'atReclaim', + 'blockDemons', + 'clear', + 'help' }) -local args = utils.processArgs({...}, validArgs) +local args = utils.processArgs({ ... }, validArgs) if moduleMode then - return + return end if args.help then - print(dfhack.script_help()) - return + print(dfhack.script_help()) + return end if args.clear then - dfhack.onStateChange.DeepEmbarkMonitor = nil - print("Cleared settings; now embarking normally.") - return + dfhack.onStateChange.DeepEmbarkMonitor = nil + print("Cleared settings; now embarking normally.") + return end if not args.depth then - qerror('Depth not specified! Enter "help deep-embark" for more information.') + qerror('Depth not specified! Enter "help deep-embark" for more information.') end local validDepths = { - ["CAVERN_1"] = true, - ["CAVERN_2"] = true, - ["CAVERN_3"] = true, - ["UNDERWORLD"] = true + ["CAVERN_1"] = true, + ["CAVERN_2"] = true, + ["CAVERN_3"] = true, + ["UNDERWORLD"] = true } if not validDepths[args.depth] then - qerror("Invalid depth: " .. args.depth) + qerror("Invalid depth: " .. args.depth) end local consoleMode = dfhack.is_interactive() -- true if the script has been called directly from the DFHack console, false if called from onLoad.init if consoleMode and not inEmbarkMode() then - -- if running from the console (not onLoad.init), abort if not currently in an embark viewscreen. - qerror('When run from the command line, this script should be run during the embark setup screens. Enter "help deep-embark" for more information.') + -- if running from the console (not onLoad.init), abort if not currently in an embark viewscreen. + qerror( + 'When run from the command line, this script should be run during the embark setup screens. Enter "help deep-embark" for more information.') end if consoleMode then - print("Embarking at: " .. tostring(args.depth)) + print("Embarking at: " .. tostring(args.depth)) end dfhack.onStateChange.DeepEmbarkMonitor = function(event) - if event == SC_VIEWSCREEN_CHANGED then -- I initially tried using SC_MAP_LOADED, but the map appears to be loaded too early when reclaiming sites - local view = dfhack.gui.getCurViewscreen() - if not consoleMode and not args.atReclaim and df.global.gametype == df.game_type.DWARF_RECLAIM then -- it's assumed that a player who chooses to run the script from console whilst reclaiming knows what they're doing, so there's no need to check for -atReclaim in this scenario - dfhack.onStateChange.DeepEmbarkMonitor = nil -- stop monitoring - return -- don't deepEmbark if running from onLoad.init and in reclaim mode without -atReclaim - elseif view._type == df.viewscreen_choose_start_sitest then -- on embark screen - if view.choosing_embark or view.choosing_reclaim then -- on a fresh embark, or on a reclaim - deepEmbark(args.depth, args.blockDemons) + if event == SC_VIEWSCREEN_CHANGED then -- I initially tried using SC_MAP_LOADED, but the map appears to be loaded too early when reclaiming sites + local view = dfhack.gui.getCurViewscreen() + if not consoleMode and not args.atReclaim and df.global.gametype == df.game_type.DWARF_RECLAIM then -- it's assumed that a player who chooses to run the script from console whilst reclaiming knows what they're doing, so there's no need to check for -atReclaim in this scenario + dfhack.onStateChange.DeepEmbarkMonitor = nil -- stop monitoring + return -- don't deepEmbark if running from onLoad.init and in reclaim mode without -atReclaim + elseif view._type == df.viewscreen_choose_start_sitest then -- on embark screen + if view.choosing_embark or view.choosing_reclaim then -- on a fresh embark, or on a reclaim + deepEmbark(args.depth, args.blockDemons) + dfhack.onStateChange.DeepEmbarkMonitor = nil + end + elseif view._type == df.viewscreen_dwarfmodest then -- we're in game. If we got here then we never got an embark screen, so this is loading a save and we abort. + dfhack.onStateChange.DeepEmbarkMonitor = nil + end + elseif event == SC_WORLD_UNLOADED then -- embark aborted dfhack.onStateChange.DeepEmbarkMonitor = nil - end - elseif view._type == df.viewscreen_dwarfmodest then -- we're in game. If we got here then we never got an embark screen, so this is loading a save and we abort. - dfhack.onStateChange.DeepEmbarkMonitor = nil end - elseif event == SC_WORLD_UNLOADED then -- embark aborted - dfhack.onStateChange.DeepEmbarkMonitor = nil - end end From 3dbaa5f065fe8bde96969c1110656a3fedf836e5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 3 Sep 2024 22:45:53 -0700 Subject: [PATCH 054/811] remove old Lua timestream migrated to a plugin --- docs/timestream.rst | 113 ---------- timestream.lua | 496 -------------------------------------------- 2 files changed, 609 deletions(-) delete mode 100644 docs/timestream.rst delete mode 100644 timestream.lua diff --git a/docs/timestream.rst b/docs/timestream.rst deleted file mode 100644 index 8b6b8eba6c..0000000000 --- a/docs/timestream.rst +++ /dev/null @@ -1,113 +0,0 @@ -timestream -========== - -.. dfhack-tool:: - :summary: Fix FPS death. - :tags: fort gameplay fps - -Do you remember when you first start a new fort, your initial 7 dwarves zip -around the screen and get things done so quickly? As a player, you never had -to wait for your initial dwarves to move across the map. Do you wish that your -fort of 200 dwarves and 800 animals could be as zippy? This tool can help. - -``timestream`` keeps the game running quickly by tweaking the game simulation -according to the frames per second that your computer can support. This means -that your dwarves spend the same amount of time relative to the in-game -calendar to do their tasks, but the time that you, the player, have to wait for -the dwarves to do get things done is reduced. The result is that the dwarves in -your fully developed fort appear as energetic as the dwarves in a newly created -fort, and mature forts are much more fun to play. - -Note that whereas your dwarves zip around like you're running at 100 FPS, the -vanilla onscreen FPS counter, if enabled, will still show a lower number. See -the `Technical details`_ section below if you're interested in what's going on -under the hood. - -Usage ------ - -:: - - enable timestream - timestream [status] - timestream set - timestream reset - -Examples --------- - -``enable timestream`` - Start adjusting the simulation to run at the currently configured apparent - FPS (default is whatever you have the FPS cap set to in the DF settings, - which is usually 100). - -``timestream set fps 50`` - Tweak the simulation so it runs at an apparent 50 frames per second. - -``timestream reset`` - Reset settings to defaults: the vanilla FPS cap with no calendar speed - advantage or disadvantage. - -Settings --------- - -:fps: Set the target simulated FPS. The default target FPS is whatever you have - the FPS cap set to in the DF settings, and the minimum is 10. Setting the - target FPS *below* your current actual FPS will have no effect. You have - to set the vanilla FPS cap for that. Set a target FPS of -1 to make no - adjustment at all to the apparent FPS of the game. - -Technical details ------------------ - -So what is this magic? How does this tool make it look like the game is -suddenly running so much faster? - -Maybe an analogy would help. Pretend you're standing at the bottom of a -staircase and you want to walk up the stairs. You can walk up one stair every -second, and there are 100 stairs, so it will take you 100 seconds to walk up -all the stairs. - -Now let's use the Hand of Armok and fiddle with reality a bit. Let's say that -instead of walking up one step, you walk up 5 steps at once. At the same time -we move the wall clock 5 seconds ahead. If you look at the clock after reaching -the top of the stairs, it will still look like it took 100 seconds, but you did -it all in fewer "steps". - -That's essentially what ``timestream`` is doing to the game. All "actions" in -DF have counters associated with them. For example, when a dwarf wants to walk -to the next tile, a counter is initialized to 8. Every "tick" of the game (the -"frame" in FPS) decrements that counter by 1. When the counter gets to zero, -the dwarf appears on the next tile. - -When ``timestream`` is active, it monitors all those counters and makes them -decrement more per tick. It then balances things out by proportionally -advancing the in-game calendar. Therefore, more "happens" per step, and DF has -to simulate fewer "steps" for the same amount of work to get done. - -The cost of this simplification is that the world becomes less "smooth". As the -discrepancy between the actual and simulated FPS grows, more and more dwarves -will move to their next tiles at *exactly* the same time. Moreover, the rate of -action completion per unit is effectively capped at the granularity of the -simulation, so very fast units (say, those in a martial trance) will lose some -of their advantage. - -Limitations ------------ - -DF does critial game tasks every 10 calendar ticks that must not be skipped, so -`timestream` cannot advance more than 9 ticks at a time. This puts an upper -limit on how much `timestream` can help. With the default target of 100 FPS, -the game will start showing signs of slowdown if the real FPS drops below about -15. The interface will also become less responsive to mouse gestures as the -real FPS drops. - -Finally, not all aspects of the game are perfectly adjusted. For example, -armies on world map will move at the same (real-time) rate regardless of -changes that ``timestream`` is making to the calendar. - -Here is a (possibly incomplete) list of game elements that are not adjusted by -``timestream`` and will appear "slow" in-game: - -- Army movement across the world map (including raids sent out from the fort) -- Liquid movement and evaporation diff --git a/timestream.lua b/timestream.lua deleted file mode 100644 index 542ed93ff9..0000000000 --- a/timestream.lua +++ /dev/null @@ -1,496 +0,0 @@ ---@module = true ---@enable = true - -local argparse = require('argparse') -local eventful = require('plugins.eventful') -local repeatutil = require("repeat-util") -local utils = require('utils') - --- set to verbosity level --- 1: dev warning messages --- 2: timeskip tracing --- 3: coverage tracing -DEBUG = DEBUG or 0 - ------------------------------------- --- state management - -local GLOBAL_KEY = 'timestream' - -local SETTINGS = { - { - name='fps', - validate=function(arg) - local val = argparse.positiveInt(arg, 'fps') - if val < 10 then qerror('target fps must be at least 10') end - return val - end, - default=function() return df.global.init.fps_cap end, - }, -} - -local function get_default_state() - local settings = {} - for _, v in ipairs(SETTINGS) do - settings[v.internal_name or v.name] = utils.getval(v.default) - end - return { - enabled=false, - settings=settings, - } -end - -state = state or get_default_state() - -function isEnabled() - return state.enabled -end - -local function persist_state() - dfhack.persistent.saveSiteData(GLOBAL_KEY, state) -end - ------------------------------------- --- business logic - --- ensure we never skip over cur_year_tick values that match this list -local TICK_TRIGGERS = { - {mod=10, rem={0}}, -- 0: season ticks (and lots of other stuff) - -- 0 mod 100: crop growth, strange mood, minimap update, rot - -- 20 mod 100: building updates - -- 40 mod 100: assign tombs to newly tomb-eligible corpses - -- 80 mod 100: incarceration updates - -- 40 mod 1000: remove excess seeds - {mod=50, rem={25, 35, 45}}, -- 25: stockpile updates - -- 35: check bags - -- 35 mod 100: job auction - -- 45: stockpile updates - {mod=100, rem={99}}, -- 99: new job creation -} - --- "owed" ticks we would like to skip at the next opportunity -timeskip_deficit = timeskip_deficit or 0.0 - --- birthday_triggers is a dense sequence of cur_year_tick values -> next unit birthday --- the sequence covers 0 .. greatest unit birthday value --- this cache is augmented when new units appear (as per the new unit event) and is cleared and --- refreshed from scratch once a year to evict data for units that are no longer active. -birthday_triggers = birthday_triggers or {} - --- coverage record for cur_year_tick % 50 so we can be sure that all items are being scanned --- (DF scans 1/50th of items every tick based on cur_year_tick % 50) --- we want every section hit at least once every 1000 ticks -tick_coverage = tick_coverage or {} - --- only throttle due to tick_coverage at most once per season tick to avoid clustering -season_tick_throttled = season_tick_throttled or false - -local function register_birthday(unit) - local btick = unit.birth_time - if btick < 0 then return end - for tick=btick,0,-1 do - if (birthday_triggers[tick] or math.huge) > btick then - birthday_triggers[tick] = btick - else - break - end - end -end - -local function check_new_unit(unit_id) - local unit = df.unit.find(unit_id) - if not unit then return end - if DEBUG >= 3 then - print('registering new unit', unit.id, dfhack.units.getReadableName(unit)) - end - register_birthday(unit) -end - -local function refresh_birthday_triggers() - birthday_triggers = {} - for _,unit in ipairs(df.global.world.units.active) do - if dfhack.units.isActive(unit) and not dfhack.units.isDead(unit) then - register_birthday(unit) - end - end -end - -local function reset_ephemeral_state() - timeskip_deficit = 0.0 - refresh_birthday_triggers() - tick_coverage = {} - season_tick_throttled = false -end - -local function get_desired_timeskip(real_fps, desired_fps) - -- minus 1 to account for the current frame - return (desired_fps / real_fps) - 1 -end - -local function clamp_coverage(timeskip) - if season_tick_throttled then return timeskip end - for val=1,timeskip do - local coverage_slot = (df.global.cur_year_tick+val) % 50 - if not tick_coverage[coverage_slot] then - season_tick_throttled = true - return val-1 - end - end - return timeskip -end - -local function record_coverage() - local coverage_slot = df.global.cur_year_tick % 50 - if DEBUG >= 3 and not tick_coverage[coverage_slot] then - print('recording coverage for slot:', coverage_slot) - end - tick_coverage[coverage_slot] = true -end - -local function get_next_birthday(next_tick) - return birthday_triggers[next_tick] or math.huge -end - -local function get_next_trigger_year_tick(next_tick) - local next_trigger_tick = math.huge - for _, trigger in ipairs(TICK_TRIGGERS) do - local cur_rem = next_tick % trigger.mod - for _, rem in ipairs(trigger.rem) do - if cur_rem <= rem then - next_trigger_tick = math.min(next_trigger_tick, next_tick + (rem - cur_rem)) - goto continue - end - end - next_trigger_tick = math.min(next_trigger_tick, next_tick + trigger.mod - cur_rem + trigger.rem[#trigger.rem]) - ::continue:: - end - return next_trigger_tick -end - -local function clamp_timeskip(timeskip) - timeskip = math.floor(timeskip) - if timeskip <= 0 then return 0 end - local next_tick = df.global.cur_year_tick + 1 - timeskip = math.min(timeskip, get_next_trigger_year_tick(next_tick)-next_tick) - timeskip = math.min(timeskip, get_next_birthday(next_tick)-next_tick) - return clamp_coverage(timeskip) -end - -local function increment_counter(obj, counter_name, timeskip) - if obj[counter_name] <= 0 then return end - obj[counter_name] = obj[counter_name] + timeskip -end - -local function decrement_counter(obj, counter_name, timeskip) - if obj[counter_name] <= 0 then return end - obj[counter_name] = math.max(1, obj[counter_name] - timeskip) -end - -local function adjust_unit_counters(unit, timeskip) - local c1 = unit.counters - decrement_counter(c1, 'think_counter', timeskip) - decrement_counter(c1, 'job_counter', timeskip) - decrement_counter(c1, 'swap_counter', timeskip) - decrement_counter(c1, 'winded', timeskip) - decrement_counter(c1, 'stunned', timeskip) - decrement_counter(c1, 'unconscious', timeskip) - decrement_counter(c1, 'suffocation', timeskip) - decrement_counter(c1, 'webbed', timeskip) - decrement_counter(c1, 'soldier_mood_countdown', timeskip) - decrement_counter(c1, 'pain', timeskip) - decrement_counter(c1, 'nausea', timeskip) - decrement_counter(c1, 'dizziness', timeskip) - local c2 = unit.counters2 - decrement_counter(c2, 'paralysis', timeskip) - decrement_counter(c2, 'numbness', timeskip) - decrement_counter(c2, 'fever', timeskip) - decrement_counter(c2, 'exhaustion', timeskip * 3) - increment_counter(c2, 'hunger_timer', timeskip) - increment_counter(c2, 'thirst_timer', timeskip) - local job = unit.job.current_job - if job and job.job_type == df.job_type.Rest then - decrement_counter(c2, 'sleepiness_timer', timeskip * 200) - elseif job and job.job_type == df.job_type.Sleep then - decrement_counter(c2, 'sleepiness_timer', timeskip * 19) - else - increment_counter(c2, 'sleepiness_timer', timeskip) - end - decrement_counter(c2, 'stomach_content', timeskip * 5) - decrement_counter(c2, 'stomach_food', timeskip * 5) - decrement_counter(c2, 'vomit_timeout', timeskip) - -- stored_fat wanders about based on other state; we can likely leave it alone and - -- not materially affect gameplay -end - --- need to manually adjust job completion_timer values for jobs that are controlled by unit actions --- with a timer of 1, which are destroyed immediately after they are created. longer-lived unit --- actions are already sufficiently handled by dfhack.units.subtractGroupActionTimers(). --- this will also decrement timers for jobs with actions that have just expired, but on average, this --- should balance out to be correct, since we're losing time when we subtract from the action timers --- and cap the value so it never drops below 1. -local function adjust_job_counter(unit, timeskip) - local job = unit.job.current_job - if not job then return end - for _,action in ipairs(unit.actions) do - if action.type == df.unit_action_type.Job or action.type == df.unit_action_type.JobRecover then - return - end - end - decrement_counter(job, 'completion_timer', timeskip) -end - --- unit needs appear to be incremented on season ticks, so we don't need to worry about those --- since the TICK_TRIGGERS check makes sure that we never skip season ticks -local function adjust_units(timeskip) - for _, unit in ipairs(df.global.world.units.active) do - if not dfhack.units.isActive(unit) then goto continue end - decrement_counter(unit, 'pregnancy_timer', timeskip) - dfhack.units.subtractGroupActionTimers(unit, timeskip, df.unit_action_type_group.All) - if not dfhack.units.isOwnGroup(unit) then goto continue end - adjust_unit_counters(unit, timeskip) - adjust_job_counter(unit, timeskip) - ::continue:: - end -end - --- behavior ascertained from in-game observation -local function adjust_activities(timeskip) - for i, act in ipairs(df.global.world.activities.all) do - for _, ev in ipairs(act.events) do - if df.activity_event_training_sessionst:is_instance(ev) then - -- no counters - elseif df.activity_event_combat_trainingst:is_instance(ev) then - -- has organize_counter at a non-zero value, but it doesn't seem to move - elseif df.activity_event_skill_demonstrationst:is_instance(ev) then - -- can be negative or positive, but always counts towards 0 - if ev.organize_counter < 0 then - ev.organize_counter = math.min(-1, ev.organize_counter + timeskip) - else - decrement_counter(ev, 'organize_counter', timeskip) - end - decrement_counter(ev, 'train_countdown', timeskip) - elseif df.activity_event_fill_service_orderst:is_instance(ev) then - -- no counters - elseif df.activity_event_individual_skill_drillst:is_instance(ev) then - -- only counts down on season ticks, nothing to do here - elseif df.activity_event_sparringst:is_instance(ev) then - decrement_counter(ev, 'countdown', timeskip * 2) - elseif df.activity_event_ranged_practicest:is_instance(ev) then - -- countdown appears to never move from 0 - decrement_counter(ev, 'countdown', timeskip) - elseif df.activity_event_harassmentst:is_instance(ev) then - if DEBUG >= 1 then - print('activity_event_harassmentst ready for analysis at index', i) - end - elseif df.activity_event_encounterst:is_instance(ev) then - if DEBUG >= 1 then - print('activity_event_encounterst ready for analysis at index', i) - end - elseif df.activity_event_reunionst:is_instance(ev) then - if DEBUG >= 1 then - print('activity_event_reunionst ready for analysis at index', i) - end - elseif df.activity_event_conversationst:is_instance(ev) then - increment_counter(ev, 'pause', timeskip) - elseif df.activity_event_guardst:is_instance(ev) then - -- no counters - elseif df.activity_event_conflictst:is_instance(ev) then - increment_counter(ev, 'inactivity_timer', timeskip) - increment_counter(ev, 'attack_inactivity_timer', timeskip) - increment_counter(ev, 'stop_fort_fights_timer', timeskip) - elseif df.activity_event_prayerst:is_instance(ev) then - decrement_counter(ev, 'timer', timeskip) - elseif df.activity_event_researchst:is_instance(ev) then - -- no counters - elseif df.activity_event_playst:is_instance(ev) then - increment_counter(ev, 'down_time_counter', timeskip) - elseif df.activity_event_worshipst:is_instance(ev) then - increment_counter(ev, 'down_time_counter', timeskip) - elseif df.activity_event_socializest:is_instance(ev) then - increment_counter(ev, 'down_time_counter', timeskip) - elseif df.activity_event_ponder_topicst:is_instance(ev) then - decrement_counter(ev, 'timer', timeskip) - elseif df.activity_event_discuss_topicst:is_instance(ev) then - decrement_counter(ev, 'timer', timeskip) - elseif df.activity_event_teach_topicst:is_instance(ev) then - decrement_counter(ev, 'time_left', timeskip) - elseif df.activity_event_readst:is_instance(ev) then - decrement_counter(ev, 'timer', timeskip) - elseif df.activity_event_writest:is_instance(ev) then - decrement_counter(ev, 'timer', timeskip) - elseif df.activity_event_copy_written_contentst:is_instance(ev) then - decrement_counter(ev, 'timer', timeskip) - elseif df.activity_event_make_believest:is_instance(ev) then - decrement_counter(ev, 'time_left', timeskip) - elseif df.activity_event_play_with_toyst:is_instance(ev) then - decrement_counter(ev, 'time_left', timeskip) - elseif df.activity_event_performancest:is_instance(ev) then - increment_counter(ev, 'current_position', timeskip) - elseif df.activity_event_store_objectst:is_instance(ev) then - if DEBUG >= 1 then - print('activity_event_store_objectst ready for analysis at index', i) - end - end - end - end -end - -local function on_tick() - record_coverage() - - if df.global.cur_year_tick % 10 == 0 then - season_tick_throttled = false - if df.global.cur_year_tick % 1000 == 0 then - if DEBUG >= 1 then - if DEBUG >= 3 then - print('checking coverage') - end - for coverage_slot=0,49 do - if not tick_coverage[coverage_slot] then - print('coverage slot not covered:', coverage_slot) - end - end - end - tick_coverage = {} - end - if df.global.cur_year_tick == 0 then - refresh_birthday_triggers() - end - end - - local real_fps = math.max(1, dfhack.internal.getUnpausedFps()) - if real_fps >= state.settings.fps then - timeskip_deficit = 0.0 - return - end - - local desired_timeskip = get_desired_timeskip(real_fps, state.settings.fps) + timeskip_deficit - local timeskip = math.max(0, clamp_timeskip(desired_timeskip)) - - -- don't let our deficit grow unbounded if we can never catch up - timeskip_deficit = math.min(desired_timeskip - timeskip, 100.0) - - if DEBUG >= 2 then - print(('cur_year_tick: %d, real_fps: %d, timeskip: (%d, +%.2f)'):format( - df.global.cur_year_tick, real_fps, timeskip, timeskip_deficit)) - end - if timeskip <= 0 then return end - - df.global.cur_year_tick = df.global.cur_year_tick + timeskip - df.global.cur_year_tick_advmode = df.global.cur_year_tick_advmode + timeskip*144 - - adjust_units(timeskip) - adjust_activities(timeskip) -end - ------------------------------------- --- hook management - -local function do_enable() - reset_ephemeral_state() - eventful.enableEvent(eventful.eventType.UNIT_NEW_ACTIVE, 10) - eventful.onUnitNewActive[GLOBAL_KEY] = check_new_unit - state.enabled = true - repeatutil.scheduleEvery(GLOBAL_KEY, 1, 'ticks', on_tick) -end - -local function do_disable() - state.enabled = false - eventful.onUnitNewActive[GLOBAL_KEY] = nil - repeatutil.cancel(GLOBAL_KEY) -end - -dfhack.onStateChange[GLOBAL_KEY] = function(sc) - if sc == SC_MAP_UNLOADED then - do_disable() - return - end - if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then - return - end - state = get_default_state() - utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) - if state.enabled then - do_enable() - end -end - ------------------------------------- --- interface - -if dfhack_flags.module then - return -end - -if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then - qerror('needs a loaded fortress map to work') -end - -local function print_status() - print(GLOBAL_KEY .. ' is ' .. (state.enabled and 'enabled' or 'not enabled')) - print() - print('settings:') - for _,v in ipairs(SETTINGS) do - print((' %15s: %s'):format(v.name, state.settings[v.internal_name or v.name])) - end - if DEBUG < 2 then return end - print() - print(('cur_year_tick: %d'):format(df.global.cur_year_tick)) - print(('timeskip_deficit: %.2f'):format(timeskip_deficit)) - if DEBUG < 3 then return end - print() - print('tick coverage:') - for coverage_slot=0,49 do - print((' slot %2d: %scovered'):format(coverage_slot, tick_coverage[coverage_slot] and '' or 'NOT ')) - end - print() - local bdays, bdays_list = {}, {} - for _, next_bday in pairs(birthday_triggers) do - if not bdays[next_bday] then - bdays[next_bday] = true - table.insert(bdays_list, next_bday) - end - end - print(('%d birthdays:'):format(#bdays_list)) - table.sort(bdays_list) - for _,bday in ipairs(bdays_list) do - print((' year tick: %d'):format(bday)) - end -end - -local function do_set(setting_name, arg) - if not setting_name or not arg then - qerror('must specify setting and value') - end - local _, setting = utils.linear_index(SETTINGS, setting_name, 'name') - if not setting then - qerror('setting not found: ' .. setting_name) - end - state.settings[setting.internal_name or setting.name] = setting.validate(arg) - print(('set %s to %s'):format(setting_name, state.settings[setting.internal_name or setting.name])) -end - -local function do_reset() - state = get_default_state() -end - -local args = {...} -local command = table.remove(args, 1) - -if dfhack_flags and dfhack_flags.enable then - if dfhack_flags.enable_state then do_enable() - else do_disable() - end -elseif command == 'set' then - do_set(args[1], args[2]) -elseif command == 'reset' then - do_reset() -elseif not command or command == 'status' then - print_status() - return -else - print(dfhack.script_help()) - return -end - -persist_state() From 0da7101cf8a3e00b24d93a24d587d49a6f9d500a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 4 Sep 2024 17:53:33 +0200 Subject: [PATCH 055/811] Move gui/journal help screen from text_editor to actual journal script --- gui/journal.lua | 1 + internal/journal/text_editor.lua | 3 +-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index cfb6f234bc..d14468129e 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -123,6 +123,7 @@ function JournalWindow:init() on_text_change=self:callback('onTextChange'), on_cursor_change=self:callback('onCursorChange'), }, + widgets.HelpButton{command="gui/journal", frame={r=0,t=1}}, widgets.Panel{ frame={l=0,r=0,b=1,h=1}, frame_inset={l=1,r=1,t=0, w=100}, diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 71521d5801..a5f27c3aff 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -123,8 +123,7 @@ function TextEditor:init() view_id='scrollbar', frame={r=0,t=1}, on_scroll=self:callback('onScrollbar') - }, - widgets.HelpButton{command="gui/journal", frame={r=0,t=0}} + } } self:setFocus(true) end From 7fd812ccbf354a00e52cc3251235154a63f1bf31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 4 Sep 2024 18:18:44 +0200 Subject: [PATCH 056/811] Add one line mode to TextEditor and use it in notes --- internal/journal/text_editor.lua | 46 +++++++++++++++++++++----------- notes.lua | 10 +++++-- test/gui/journal.lua | 5 +++- 3 files changed, 43 insertions(+), 18 deletions(-) diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index a5f27c3aff..cf15776d46 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -94,6 +94,7 @@ TextEditor.ATTRS{ select_pen = COLOR_CYAN, on_text_change = DEFAULT_NIL, on_cursor_change = DEFAULT_NIL, + one_line_mode = false, debug = false } @@ -104,25 +105,27 @@ function TextEditor:init() TextEditorView{ view_id='text_area', frame={l=0,r=3,t=0}, - text = self.init_text, + text=self.init_text, - text_pen = self.text_pen, - ignore_keys = self.ignore_keys, - select_pen = self.select_pen, - debug = self.debug, + text_pen=self.text_pen, + ignore_keys=self.ignore_keys, + select_pen=self.select_pen, + debug=self.debug, + one_line_mode=self.one_line_mode, - on_text_change = function (val) + on_text_change=function (val) self:updateLayout() if self.on_text_change then self.on_text_change(val) end end, - on_cursor_change = self:callback('onCursorChange') + on_cursor_change=self:callback('onCursorChange') }, widgets.Scrollbar{ view_id='scrollbar', frame={r=0,t=1}, - on_scroll=self:callback('onScrollbar') + on_scroll=self:callback('onScrollbar'), + visible=not self.one_line_mode } } self:setFocus(true) @@ -251,6 +254,7 @@ TextEditorView.ATTRS{ on_cursor_change = DEFAULT_NIL, enable_cursor_blink = true, debug = false, + one_line_mode = false, history_size = 10, } @@ -273,6 +277,8 @@ function TextEditorView:init() bold=true }) + self.text = self:normalizeText(self.text) + self.wrapped_text = wrapped_text.WrappedText{ text=self.text, wrap_width=256 @@ -281,6 +287,14 @@ function TextEditorView:init() self.history = TextEditorHistory{history_size=self.history_size} end +function TextEditorView:normalizeText(text) + if self.one_line_mode then + return text:gsub("\r?\n", "") + end + + return text +end + function TextEditorView:setRenderStartLineY(render_start_line_y) self.render_start_line_y = render_start_line_y end @@ -410,7 +424,7 @@ end function TextEditorView:setText(text) local changed = self.text ~= text - self.text = text + self.text = self:normalizeText(text) self:recomputeLines() @@ -782,12 +796,14 @@ end function TextEditorView:onTextManipulationInput(keys) if keys.SELECT then -- handle enter - self.history:store( - HISTORY_ENTRY.WHITESPACE_BLOCK, - self.text, - self.cursor - ) - self:insert(NEWLINE) + if not self.one_line_mode then + self.history:store( + HISTORY_ENTRY.WHITESPACE_BLOCK, + self.text, + self.cursor + ) + self:insert(NEWLINE) + end return true diff --git a/notes.lua b/notes.lua index 90e50b8610..b025f01855 100644 --- a/notes.lua +++ b/notes.lua @@ -7,7 +7,12 @@ local overlay = require('plugins.overlay') local guidm = require('gui.dwarfmode') local text_editor = reqscript('internal/journal/text_editor') -local green_pin = dfhack.textures.loadTileset('hack/data/art/note-green-pin.png', 32, 32, true) +local green_pin = dfhack.textures.loadTileset( + 'hack/data/art/note_green_pin_map.png', + 32, + 32, + true +) NotesOverlay = defclass(NotesOverlay, overlay.OverlayWidget) NotesOverlay.ATTRS{ @@ -171,7 +176,8 @@ function NoteManager:init() frame={t=1,h=3}, frame_style=gui.FRAME_INTERIOR, init_text=self.note and self.note.point.name or '', - init_cursor=1 + init_cursor=1, + one_line_mode=true }, widgets.HotkeyLabel { key='CUSTOM_ALT_C', diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 29fcc1d694..d845aef20e 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -77,7 +77,7 @@ local function arrange_empty_journal(options) gui_journal.main({ save_prefix='test:', save_on_change=options.save_on_change or false, - save_layout=options.allow_layout_restore or false + save_layout=options.allow_layout_restore or false, }) local journal = gui_journal.view @@ -3068,3 +3068,6 @@ function test.show_tutorials_on_first_use() expect.str_find('Section 1\n', read_rendered_text(toc_panel)); journal:dismiss() end + +-- TODO: separate journal tests from TextEditor tests +-- add "one_line_mode" tests From 30feadb4689353a0c66e62caf445f34f75f9456e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 4 Sep 2024 19:21:31 +0200 Subject: [PATCH 057/811] Add basic documentation for notes tool --- docs/notes.rst | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 docs/notes.rst diff --git a/docs/notes.rst b/docs/notes.rst new file mode 100644 index 0000000000..3bfc5304ef --- /dev/null +++ b/docs/notes.rst @@ -0,0 +1,42 @@ +notes +===== + +.. dfhack-tool:: + :summary: Manage map-specific notes + :tags: fort interface map + +The `notes` tool enables players to annotate specific tiles +on the Dwarf Fortress game map with customizable notes. + +Each note is displayed as a green pin on the map and includes a one-line title and a detailed comment. + +It can be used to e.g.: + - marking plans for future constructions + - explaining mechanisms or traps + - noting historical events + +Usage +----- + +:: + + notes add + +Add new note in the current position of the keyboard cursor. + +Creating a Note +~~~~~~~~~~~~~~~ +1. Use the keyboard cursor to select the desired map tile where you want to place a note. +2. Execute ``notes add`` via the DFHack console. +3. In the pop-up dialog, fill in the note's title and detailed comment. +4. Press :kbd:`Alt` + :kbd:`S` to create the note. + +Editing or Deleting a Note +~~~~~~~~~~~~~~~~~~~~~~~~~~ +- Click on the green pin representing the note directly on the map. +- A dialog will appear, offering options to edit the title or comment, or to delete the note entirely. + +Managing Notes Visibility +~~~~~~~~~~~~~~~~~~~~~~~~~ +- Access the ``DFHack Control Panel`` / ``UI Overlays`` tab. +- Toggle the ``notes.map-notes`` overlay to show or hide the notes on the map. From 0f5ab80dcceec6b074346000c0d811e1e2797c50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 4 Sep 2024 19:24:34 +0200 Subject: [PATCH 058/811] Add notes tool to changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 5e1761acba..2cee1f410c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,7 @@ Template for new versions: - `embark-anyone`: allows you to embark as any civilization, including dead and non-dwarven ones - `idle-crafting`: allow dwarves to independently satisfy their need to craft objects - `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationships +- `notes`: Manage map-specific notes ## New Features - `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them From 9176de85e2dc11ec9c0119e5064a65e7ddb86f3b Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Wed, 4 Sep 2024 13:19:04 -0700 Subject: [PATCH 059/811] Update position.lua --- position.lua | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/position.lua b/position.lua index 3ca3bdd543..b0575f2c46 100644 --- a/position.lua +++ b/position.lua @@ -33,18 +33,18 @@ local months = { --Adventurer mode counts 86400 ticks to a day and 29030400 ticks per year --Twelve months per year, 28 days to every month, 336 days per year -local julian_day = math.floor(df.global.cur_year_tick / 1200) + 1 -local month = math.floor(julian_day / 28) + 1 --days and months are 1-indexed +local julian_day = df.global.cur_year_tick // 1200 + 1 +local month = julian_day // 28 + 1 --days and months are 1-indexed local day = julian_day % 28 -local time_of_day = math.floor(df.global.cur_year_tick_advmode / 336) +local time_of_day = df.global.cur_year_tick_advmode // 336 local second = time_of_day % 60 -local minute = math.floor(time_of_day / 60) % 60 -local hour = math.floor(time_of_day / 3600) % 24 +local minute = time_of_day // 60 % 60 +local hour = time_of_day // 3600 % 24 print('Time:') -print(' The time is '..string.format('%02d:%02d:%02d', hour, minute, second)) -print(' The date is '..string.format('%05d-%02d-%02d', df.global.cur_year, month, day)) +print((' The time is %02d:%02d:%02d'):format(hour, minute, second)) +print((' The date is %03d-%02d-%02d'):format(df.global.cur_year, month, day)) print(' It is the month of '..months[month]) local eras = df.global.world.history.eras @@ -55,7 +55,30 @@ end print('Place:') print(' The z-level is z='..df.global.window_z) print(' The cursor is at x='..cursor.x..', y='..cursor.y) -print(' The window is '..df.global.gps.dimx..' tiles wide and '..df.global.gps.dimy..' tiles high') -if df.global.gps.mouse_x == -1 then print(' The mouse is not in the DF window') else -print(' The mouse is at x='..df.global.gps.mouse_x..', y='..df.global.gps.mouse_y..' within the window') end ---TODO: print(' The fortress is at '..x, y..' on the world map ('..worldsize..' square)') +print(' The window is '..df.global.gps.dimx..' tiles wide and '..df.global.gps.dimy..' tiles high.') + +if df.global.gps.mouse_x < 0 then + print(' The mouse is not in the DF window.') +else + print(' The mouse is at x='..df.global.gps.mouse_x..', y='..df.global.gps.mouse_y..' within the window.') +end + +local wd = df.global.world.world_data +local site = dfhack.world.getCurrentSite() +if site then + print((' The current site is at x=%d, y=%d on the world map (%dx%d).'): + format(site.pos.x, site.pos.y, wd.world_width, wd.world_height)) +elseif dfhack.world.isAdventureMode() then + local ax, ay = -1, -1 + for _,army in ipairs(df.global.world.armies.all) do + if army.flags.player then + ax, ay = army.pos.x // 48, army.pos.y // 48 + break + end + end + if ax < 0 then + ax, ay = wd.midmap_data.adv_region_x, wd.midmap_data.adv_region_y + end + print((' The adventurer is at x=%d, y=%d on the world map (%dx%d).'): + format(ax, ay, wd.world_width, wd.world_height)) +end From e6f4a5512f4b3d59c309ae99df0bfbca8e0b364c Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Wed, 4 Sep 2024 13:24:13 -0700 Subject: [PATCH 060/811] Update position.rst --- docs/position.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/position.rst b/docs/position.rst index 72ab716b5d..679cbc80e0 100644 --- a/docs/position.rst +++ b/docs/position.rst @@ -3,11 +3,13 @@ position .. dfhack-tool:: :summary: Report cursor and mouse position, along with other info. - :tags: fort inspection map + :tags: adventure fort inspection map This tool reports the current date, clock time, month, season, and historical era. It also reports the keyboard cursor position (or just the z-level if no -active cursor), window size, and mouse location on the screen. +active cursor), window size, and mouse location on the screen. If a site is +loaded, it prints the world coordinates of the site, else the world +coordinates of the adventurer. Can also be used to copy the current keyboard cursor position for later use. From adf5242cd6af54c64bcbebc7f3a5e61a1cbc6a67 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Wed, 4 Sep 2024 13:27:15 -0700 Subject: [PATCH 061/811] Update changelog.txt --- changelog.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 137592f1ae..0754d13292 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,10 +17,6 @@ Template for new versions: ## New Features ## Fixes -- `gui/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing." -Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. -- `modtools/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing"s. -Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. ## Misc Improvements @@ -48,12 +44,16 @@ Items will now spawn correctly, and will be of the creature type and creature ca - `gui/design`: don't overcount "affected tiles" for Line & Freeform drawing tools - `deep-embark`: fix error when embarking where there is no land to stand on (e.g. when embarking in the ocean with `gui/embark-anywhere` - `deep-embark`: fix failure to transport units and items when embarking where there is no room to spawn the starting wagon +- `gui/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing." +Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. +- `modtools/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing"s. +Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. ## Misc Improvements - `gui/sitemap`: show whether a unit is friendly, hostile, or wild - `gui/sitemap`: show whether a unit is caged - `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma -- `position`: report current historical era (e.g., "Age of Myth") +- `position`: report current historical era (e.g., "Age of Myth") and site/adventurer world coords ## Documentation - `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses From 9e4b68366d037dfb0379704a16b871b3cb25f983 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Wed, 4 Sep 2024 13:44:26 -0700 Subject: [PATCH 062/811] Update changelog.txt --- changelog.txt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index 0754d13292..71a7e4ed18 100644 --- a/changelog.txt +++ b/changelog.txt @@ -44,10 +44,8 @@ Template for new versions: - `gui/design`: don't overcount "affected tiles" for Line & Freeform drawing tools - `deep-embark`: fix error when embarking where there is no land to stand on (e.g. when embarking in the ocean with `gui/embark-anywhere` - `deep-embark`: fix failure to transport units and items when embarking where there is no room to spawn the starting wagon -- `gui/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing." -Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. -- `modtools/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing"s. -Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. +- `gui/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing." Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. +- `modtools/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing"s. Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. ## Misc Improvements - `gui/sitemap`: show whether a unit is friendly, hostile, or wild From cc223f1991b00f0e9f2395a9b7b412a612c1eec2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 5 Sep 2024 08:09:17 +0200 Subject: [PATCH 063/811] Polishing notes documentation --- docs/notes.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/notes.rst b/docs/notes.rst index 3bfc5304ef..faed2c8fbf 100644 --- a/docs/notes.rst +++ b/docs/notes.rst @@ -2,7 +2,7 @@ notes ===== .. dfhack-tool:: - :summary: Manage map-specific notes + :summary: Manage map-specific notes. :tags: fort interface map The `notes` tool enables players to annotate specific tiles @@ -25,18 +25,18 @@ Usage Add new note in the current position of the keyboard cursor. Creating a Note -~~~~~~~~~~~~~~~ +--------------- 1. Use the keyboard cursor to select the desired map tile where you want to place a note. 2. Execute ``notes add`` via the DFHack console. 3. In the pop-up dialog, fill in the note's title and detailed comment. 4. Press :kbd:`Alt` + :kbd:`S` to create the note. Editing or Deleting a Note -~~~~~~~~~~~~~~~~~~~~~~~~~~ +-------------------------- - Click on the green pin representing the note directly on the map. - A dialog will appear, offering options to edit the title or comment, or to delete the note entirely. Managing Notes Visibility -~~~~~~~~~~~~~~~~~~~~~~~~~ -- Access the ``DFHack Control Panel`` / ``UI Overlays`` tab. +------------------------- +- Access the `gui/control-panel` / ``UI Overlays`` tab. - Toggle the ``notes.map-notes`` overlay to show or hide the notes on the map. From fc7efa3d45f379d782531c6f1a68e978399eb1c1 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 5 Sep 2024 15:10:16 -0700 Subject: [PATCH 064/811] Requested changes * Update position.lua * Update position.rst * Update changelog.txt --- changelog.txt | 2 +- docs/position.rst | 4 ++-- position.lua | 36 ++++++++++++++++++++++++------------ 3 files changed, 27 insertions(+), 15 deletions(-) diff --git a/changelog.txt b/changelog.txt index 71a7e4ed18..459c839690 100644 --- a/changelog.txt +++ b/changelog.txt @@ -51,7 +51,7 @@ Template for new versions: - `gui/sitemap`: show whether a unit is friendly, hostile, or wild - `gui/sitemap`: show whether a unit is caged - `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma -- `position`: report current historical era (e.g., "Age of Myth") and site/adventurer world coords +- `position`: report current historical era (e.g., "Age of Myth"), site/adventurer world coords, and mouse map tile coords ## Documentation - `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses diff --git a/docs/position.rst b/docs/position.rst index 679cbc80e0..23a8cfe1e0 100644 --- a/docs/position.rst +++ b/docs/position.rst @@ -8,8 +8,8 @@ position This tool reports the current date, clock time, month, season, and historical era. It also reports the keyboard cursor position (or just the z-level if no active cursor), window size, and mouse location on the screen. If a site is -loaded, it prints the world coordinates of the site, else the world -coordinates of the adventurer. +loaded, it prints the world coordinates of the site. If not, it prints the world +coordinates of the adventurer (if applicable). Can also be used to copy the current keyboard cursor position for later use. diff --git a/position.lua b/position.lua index b0575f2c46..de6248f62f 100644 --- a/position.lua +++ b/position.lua @@ -54,31 +54,43 @@ end print('Place:') print(' The z-level is z='..df.global.window_z) -print(' The cursor is at x='..cursor.x..', y='..cursor.y) -print(' The window is '..df.global.gps.dimx..' tiles wide and '..df.global.gps.dimy..' tiles high.') -if df.global.gps.mouse_x < 0 then - print(' The mouse is not in the DF window.') +if cursor.x < 0 then + print(' The keyboard cursor is inactive.') else - print(' The mouse is at x='..df.global.gps.mouse_x..', y='..df.global.gps.mouse_y..' within the window.') + print(' The keyboard cursor is at x='..cursor.x..', y='..cursor.y) +end + +local x, y = dfhack.screen.getWindowSize() +print(' The window is '..x..' tiles wide and '..y..' tiles high.') + +x, y = dfhack.screen.getMousePos() +if x then + print(' The mouse is at x='..x..', y='..y..' within the window.') + local pos = dfhack.gui.getMousePos() + if pos then + print(' The mouse is over map tile x='..pos.x..', y='..pos.y) + end +else + print(' The mouse is not in the DF window.') end local wd = df.global.world.world_data local site = dfhack.world.getCurrentSite() if site then - print((' The current site is at x=%d, y=%d on the world map (%dx%d).'): + print((' The current site is at x=%d, y=%d on the %dx%d world map.'): format(site.pos.x, site.pos.y, wd.world_width, wd.world_height)) elseif dfhack.world.isAdventureMode() then - local ax, ay = -1, -1 + x, y = -1, -1 for _,army in ipairs(df.global.world.armies.all) do if army.flags.player then - ax, ay = army.pos.x // 48, army.pos.y // 48 + x, y = army.pos.x // 48, army.pos.y // 48 break end end - if ax < 0 then - ax, ay = wd.midmap_data.adv_region_x, wd.midmap_data.adv_region_y + if x < 0 then + x, y = wd.midmap_data.adv_region_x, wd.midmap_data.adv_region_y end - print((' The adventurer is at x=%d, y=%d on the world map (%dx%d).'): - format(ax, ay, wd.world_width, wd.world_height)) + print((' The adventurer is at x=%d, y=%d on the %dx%d world map.'): + format(x, y, wd.world_width, wd.world_height)) end From 8ece866908ccb45ca4e3c270d36e5edecab02470 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Sep 2024 12:45:39 -0700 Subject: [PATCH 065/811] handle case where minecart is assigned but is missing --- assign-minecarts.lua | 18 ++++++++---------- changelog.txt | 1 + 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/assign-minecarts.lua b/assign-minecarts.lua index 3cff2cd726..5f6550c266 100644 --- a/assign-minecarts.lua +++ b/assign-minecarts.lua @@ -2,6 +2,7 @@ --@ module = true local argparse = require('argparse') +local utils = require('utils') function get_free_vehicles() local free_vehicles = {} @@ -13,16 +14,12 @@ function get_free_vehicles() return free_vehicles end -local function has_minecart(route) - return #route.vehicle_ids > 0 -end - local function has_stops(route) return #route.stops > 0 end local function get_minecart(route) - if not has_minecart(route) then return end + if #route.vehicle_ids == 0 then return end local vehicle = utils.binsearch(df.global.world.vehicles.active, route.vehicle_ids[0], 'id') if not vehicle then return end return df.item.find(vehicle.item_id) @@ -37,8 +34,9 @@ local function get_id_and_name(route) end local function assign_minecart_to_route(route, quiet, minecart) - if has_minecart(route) then - return get_minecart(route) + local assigned_minecart = get_minecart(route) + if assigned_minecart then + return assigned_minecart end if not has_stops(route) then if not quiet then @@ -99,7 +97,7 @@ local function list() for _,route in ipairs(routes) do print(('%-8d %-9s %-9s %s') :format(route.id, - has_minecart(route) and 'yes' or 'NO', + get_minecart(route) and 'yes' or 'NO', has_stops(route) and 'yes' or 'NO', get_name(route))) end @@ -113,7 +111,7 @@ local function all(quiet) local minecarts, idx = get_free_vehicles(), 1 local routes = df.global.plotinfo.hauling.routes for _,route in ipairs(routes) do - if has_minecart(route) then + if get_minecart(route) then goto continue end if not assign_minecart_to_route(route, quiet, minecarts[idx]) then @@ -148,7 +146,7 @@ local function main(args) local route = get_route_by_id(requested_route_id) if not route then dfhack.printerr('route id not found: '..requested_route_id) - elseif has_minecart(route) then + elseif get_minecart(route) then if not quiet then print(('Route %s already has a minecart assigned.') :format(get_id_and_name(route))) diff --git a/changelog.txt b/changelog.txt index 52c4700c93..50b06fbbd9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -53,6 +53,7 @@ Template for new versions: - `gui/sitemap`: show whether a unit is caged - `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma - `position`: report current historical era (e.g., "Age of Myth"), site/adventurer world coords, and mouse map tile coords +- `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) ## Documentation - `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses From b7d8a37fef2ac3dca790cf721e11e8dd24e64965 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Sep 2024 12:58:36 -0700 Subject: [PATCH 066/811] properly unassign old minecart --- assign-minecarts.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/assign-minecarts.lua b/assign-minecarts.lua index 5f6550c266..53e498d2b7 100644 --- a/assign-minecarts.lua +++ b/assign-minecarts.lua @@ -55,6 +55,12 @@ local function assign_minecart_to_route(route, quiet, minecart) return false end end + for _,vehicle_id in ipairs(route.vehicle_ids) do + local vehicle = utils.binsearch(df.global.world.vehicles.all, vehicle_id, 'id') + if vehicle then vehicle.route_id = -1 end + end + route.vehicle_ids:resize(0) + route.vehicle_stops:resize(0) route.vehicle_ids:insert('#', minecart.id) route.vehicle_stops:insert('#', 0) minecart.route_id = route.id From 694432dcf403ce0646091c55aaa614e890870250 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Sep 2024 13:27:21 -0700 Subject: [PATCH 067/811] fix overcounting of total tiles when there are invalid tiles --- internal/quickfort/building.lua | 2 +- internal/quickfort/preview.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/quickfort/building.lua b/internal/quickfort/building.lua index 9f258c7e77..147442a656 100644 --- a/internal/quickfort/building.lua +++ b/internal/quickfort/building.lua @@ -532,7 +532,7 @@ function check_tiles_and_extents(ctx, buildings) -- as invalid; the building can still build around it owns_preview = quickfort_preview.set_preview_tile(ctx, pos, - is_valid_tile or db_entry.has_extents) + is_valid_tile or db_entry.has_extents or false) if not is_valid_tile then log('tile not usable: (%d, %d, %d)', pos.x, pos.y, pos.z) col[extent_y] = false diff --git a/internal/quickfort/preview.lua b/internal/quickfort/preview.lua index ea98b9de76..a1a7afd5f2 100644 --- a/internal/quickfort/preview.lua +++ b/internal/quickfort/preview.lua @@ -11,7 +11,7 @@ end function set_preview_tile(ctx, pos, is_valid_tile, override) local preview = ctx.preview if not preview then return false end - local preview_row = ensure_key(ensure_key(ctx.preview.tiles, pos.z), pos.y) + local preview_row = ensure_keys(ctx.preview.tiles, pos.z, pos.y) if preview_row[pos.x] == nil then preview.total_tiles = preview.total_tiles + 1 end From 66e169e898a8d5dcf5051b111ffeb4b866d3f96b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Sep 2024 13:27:42 -0700 Subject: [PATCH 068/811] colorize loaded blueprint name --- gui/quickfort.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/quickfort.lua b/gui/quickfort.lua index eae4c701fd..aff6824e00 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -314,7 +314,7 @@ function Quickfort:init() widgets.ResizingPanel{autoarrange_subviews=true, subviews={ widgets.Label{text='Current blueprint:'}, widgets.WrappedLabel{ - text_pen=COLOR_GREY, + text_pen=COLOR_CYAN, text_to_wrap=self:callback('get_blueprint_name')} }}, widgets.ResizingPanel{autoarrange_subviews=true, subviews={ From 886e91b4eaedf28da3324d5c1c670f809a0d3986 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Sep 2024 16:11:31 -0700 Subject: [PATCH 069/811] clean up algorithm so it only scans relevant items --- docs/fix/dry-buckets.rst | 4 ++++ fix/dry-buckets.lua | 44 ++++++++++++++++++++++++---------------- 2 files changed, 30 insertions(+), 18 deletions(-) diff --git a/docs/fix/dry-buckets.rst b/docs/fix/dry-buckets.rst index 311e3e6c2e..9d3a7d4e0b 100644 --- a/docs/fix/dry-buckets.rst +++ b/docs/fix/dry-buckets.rst @@ -12,6 +12,10 @@ water from them. This tool also fixes over-full buckets that are blocking well operations. +If enabled in `gui/control-panel` (it is enabled by default), this fix is +periodically run automaticaly, so you should not normally need to run it +manually. + Usage ----- diff --git a/fix/dry-buckets.lua b/fix/dry-buckets.lua index 27834abb82..db98f0f719 100644 --- a/fix/dry-buckets.lua +++ b/fix/dry-buckets.lua @@ -1,35 +1,43 @@ local argparse = require("argparse") -local quiet = false - -local emptied = 0 -local in_building = 0 local water_type = dfhack.matinfo.find('WATER').type +local quiet = false argparse.processArgsGetopt({...}, { {'q', 'quiet', handler=function() quiet = true end}, }) -for _,item in ipairs(df.global.world.items.other.IN_PLAY) do - local container = dfhack.items.getContainer(item) - if container - and container:getType() == df.item_type.BUCKET - and not (container.flags.in_job) - and item:getMaterial() == water_type - and item:getType() == df.item_type.LIQUID_MISC - and not (item.flags.in_job) - then - if container.flags.in_building or item.flags.in_building then - in_building = in_building + 1 +local emptied = 0 +local in_building = 0 +for _,item in ipairs(df.global.world.items.other.BUCKET) do + if item.flags.in_job then goto continue end + local emptied_bucket = false + local freed_in_building = false + for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do + if not contained_item.flags.in_job and + contained_item:getMaterial() == water_type and + contained_item:getType() == df.item_type.LIQUID_MISC + then + if item.flags.in_building or contained_item.flags.in_building then + freed_in_building = true + end + -- ok to remove item while iterating since we're iterating through copy of the vector + dfhack.items.remove(contained_item) + emptied_bucket = true end - dfhack.items.remove(item) + end + if emptied_bucket then emptied = emptied + 1 end + if freed_in_building then + in_building = in_building + 1 + end + ::continue:: end if not quiet then - print('Emptied '..emptied..' buckets.') - if emptied > 0 then + print(('Emptied %d buckets.'):format(emptied)) + if in_building > 0 then print(('Unclogged %d wells.'):format(in_building)) end end From db96f839ccb95aadb46b6f5b137c2b5f19b36e50 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Sep 2024 16:14:30 -0700 Subject: [PATCH 070/811] prompt DF to recheck requests for give water jobs --- fix/dry-buckets.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/fix/dry-buckets.lua b/fix/dry-buckets.lua index db98f0f719..71d2c50468 100644 --- a/fix/dry-buckets.lua +++ b/fix/dry-buckets.lua @@ -28,6 +28,7 @@ for _,item in ipairs(df.global.world.items.other.BUCKET) do end if emptied_bucket then emptied = emptied + 1 + df.global.plotinfo.flags.recheck_aid_requests = true end if freed_in_building then in_building = in_building + 1 From 836ad2faf0323509cf47d7915272e27a5d0b78bb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Sep 2024 16:19:11 -0700 Subject: [PATCH 071/811] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 50b06fbbd9..5181b1d3a3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -54,6 +54,7 @@ Template for new versions: - `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma - `position`: report current historical era (e.g., "Age of Myth"), site/adventurer world coords, and mouse map tile coords - `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) +- `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use ## Documentation - `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses From bd41b1cac8a1b49d559d427584a55702d636ab6b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Sep 2024 16:38:22 -0700 Subject: [PATCH 072/811] swap order of item origins options --- internal/caravan/common.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index cbf95eb809..135762376c 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -488,8 +488,8 @@ function get_info_widgets(self, export_agreements, strict_ethical_bins_default, label='Item origins:', options={ {label='All', value='all', pen=COLOR_GREEN}, - {label='Foreign-made only', value='foreign', pen=COLOR_YELLOW}, {label='Fort-made only', value='local', pen=COLOR_BLUE}, + {label='Foreign-made only', value='foreign', pen=COLOR_YELLOW}, }, on_change=function() self:refresh_list() end, }, From 2062b2a0e1301efc640b51c4c8dcbb845fc4407e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Sep 2024 16:39:05 -0700 Subject: [PATCH 073/811] changelog editing --- changelog.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5181b1d3a3..7fd15debc4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,10 +27,10 @@ Template for new versions: # Future ## New Tools -- `embark-anyone`: allows you to embark as any civilization, including dead and non-dwarven ones +- `embark-anyone`: allows you to embark as any civilization, including dead and non-dwarven civs - `idle-crafting`: allow dwarves to independently satisfy their need to craft objects - `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationships -- `notes`: Manage map-specific notes +- `notes`: manage map-specific notes ## New Features - `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them From 03c8e9a6755e40463debd12de932295b80a1448c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Sep 2024 16:52:50 -0700 Subject: [PATCH 074/811] changelog editing pass --- changelog.txt | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 7fd15debc4..7bd16a4465 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,22 +37,21 @@ Template for new versions: - `caravan`: If you have managed to select an item that is ethically unacceptable to the merchant, an "Ethics warning" badge will now appear next to the "Trade" button. Clicking on the badge will show you which items that you have selected are problematic. The dialog has a button that you can click to deselect the problematic items in the trade list. - `confirm`: If you have ethically unacceptable items selected for trade, the "Are you sure you want to trade" confirmation will warn you about them - `quickfort`: ``#zone`` blueprints now integrated with `preserve-rooms` so you can create a zone and automatically assign it to a noble or administrative role -- `position`: option to copy cursor position to clipboard ## Fixes -- `timestream`: ensure child growth events (e.g. becoming an adult) are not skipped +- `timestream`: ensure child growth events (that is, a child's transition to adulthood) are not skipped; existing "overage" children will be automatically fixed within a year - `empty-bin`: ``--liquids`` option now correctly empties containers filled with LIQUID_MISC (like lye) - `gui/design`: don't overcount "affected tiles" for Line & Freeform drawing tools -- `deep-embark`: fix error when embarking where there is no land to stand on (e.g. when embarking in the ocean with `gui/embark-anywhere` +- `deep-embark`: fix error when embarking where there is no land to stand on (e.g. when embarking in the ocean with `gui/embark-anywhere`) - `deep-embark`: fix failure to transport units and items when embarking where there is no room to spawn the starting wagon -- `gui/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing." Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. -- `modtools/create-item`: fix items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing"s. Items will now spawn correctly, and will be of the creature type and creature caste that selected by the user. Items of these types will also stack correctly when needed. +- `gui/create-item`, `modtools/create-item`: items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing" and will now stack correctly ## Misc Improvements - `gui/sitemap`: show whether a unit is friendly, hostile, or wild - `gui/sitemap`: show whether a unit is caged - `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma - `position`: report current historical era (e.g., "Age of Myth"), site/adventurer world coords, and mouse map tile coords +- `position`: option to copy keyboard cursor position to the clipboard - `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) - `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use From 64af1a45bdbf5689569fce33de381a9916a14d1e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Sep 2024 17:12:29 -0700 Subject: [PATCH 075/811] add window title and start cursor at end of string --- notes.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/notes.lua b/notes.lua index b025f01855..99aab4a748 100644 --- a/notes.lua +++ b/notes.lua @@ -162,6 +162,7 @@ function NoteManager:init() widgets.Window{ frame={w=35,h=20}, frame_inset={t=1}, + frame_title='Notes', resizable=true, subviews={ widgets.HotkeyLabel { @@ -176,7 +177,7 @@ function NoteManager:init() frame={t=1,h=3}, frame_style=gui.FRAME_INTERIOR, init_text=self.note and self.note.point.name or '', - init_cursor=1, + init_cursor=self.note and #self.note.point.name+1 or 1, one_line_mode=true }, widgets.HotkeyLabel { From 25ad9717c561027bf5b3da9e0b9eb618b266494e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Sep 2024 10:31:58 -0700 Subject: [PATCH 076/811] apply suggestions from code review and ensure histfig data is consistent --- changelog.txt | 2 ++ docs/rejuvenate.rst | 11 ++++---- rejuvenate.lua | 65 +++++++++++++++++++++------------------------ 3 files changed, 38 insertions(+), 40 deletions(-) diff --git a/changelog.txt b/changelog.txt index 7bd16a4465..482ed94054 100644 --- a/changelog.txt +++ b/changelog.txt @@ -45,6 +45,8 @@ Template for new versions: - `deep-embark`: fix error when embarking where there is no land to stand on (e.g. when embarking in the ocean with `gui/embark-anywhere`) - `deep-embark`: fix failure to transport units and items when embarking where there is no room to spawn the starting wagon - `gui/create-item`, `modtools/create-item`: items of type "VERMIN", "PET", "REMANS", "FISH", "RAW FISH", and "EGG" no longer spawn creature item "nothing" and will now stack correctly +- `rejuvenate`: don't set a lifespan limit for creatures that are immortal (e.g. elves, goblins) +- `rejuvenate`: properly disconnect babies from mothers when aging babies up to adults ## Misc Improvements - `gui/sitemap`: show whether a unit is friendly, hostile, or wild diff --git a/docs/rejuvenate.rst b/docs/rejuvenate.rst index 754510921c..73d6eebf8f 100644 --- a/docs/rejuvenate.rst +++ b/docs/rejuvenate.rst @@ -7,7 +7,8 @@ rejuvenate If your most valuable citizens are getting old, this tool can save them. It decreases the age of the selected dwarf to 20 years, or to the age specified. -Age is only increased using the --force option. +Age can only be increased (e.g. when this tool is run on babies or children) +if the ``--force`` option is specified. Usage ----- @@ -24,7 +25,7 @@ Examples ``rejuvenate --all`` Set the age of all dwarves over 20 to 20. ``rejuvenate --all --force`` - Set the age of all dwarves (including babies) to 20. + Set the age of all dwarves (including children and babies) to 20. ``rejuvenate --age 149 --force`` Set the age of the selected dwarf to 149, even if they are younger. @@ -34,9 +35,9 @@ Options ``--all`` Rejuvenate all citizens, not just the selected one. ``--age `` - Sets the target to the age specified. If this is not set, the target age is 20. + Sets the target to the age specified. If this is not set, the target age defaults to ``20``. ``--force`` - Set age for units under the specified age to the specified age. Useful if there are too - many babies around... + Set age for units under the specified age to the specified age. Useful if + there are too many babies around... ``--dry-run`` Only list units that would be changed; don't actually change ages. diff --git a/rejuvenate.lua b/rejuvenate.lua index ddebbf1475..2ecdc7183e 100644 --- a/rejuvenate.lua +++ b/rejuvenate.lua @@ -1,65 +1,60 @@ --- set age of selected unit --- by vjek --@ module = true local utils = require('utils') -function rejuvenate(unit, force, dry_run, age) +local ANY_BABY = df.global.world.units.other.ANY_BABY + +-- called by armoks-blessing +function rejuvenate(unit, quiet, force, dry_run, age) + age = age or 20 local current_year = df.global.cur_year - if not age then - age = 20 - end local new_birth_year = current_year - age - local name = dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) + local new_old_year = unit.old_year < 0 and -1 or math.max(unit.old_year, new_birth_year + 160) + local name = dfhack.df2console(dfhack.units.getReadableName(unit)) if unit.birth_year > new_birth_year and not force then - print(name .. ' is under ' .. age .. ' years old. Use --force to force.') + if not quiet then + dfhack.printerr(name .. ' is under ' .. age .. ' years old. Use --force to force.') + end return end if dry_run then print('would change: ' .. name) return end + + local hf = df.historical_figure.find(unit.hist_figure_id) unit.birth_year = new_birth_year - if unit.old_year < new_birth_year + 160 then - unit.old_year = new_birth_year + 160 - end + if hf then hf.born_year = new_birth_year end + unit.old_year = new_old_year + if hf then hf.old_year = new_old_year end + if unit.profession == df.profession.BABY or unit.profession == df.profession.CHILD then if unit.profession == df.profession.BABY then - local leftoverUnits = {} - local shiftedLeftoverUnits = {} - -- create a copy - local babyUnits = df.global.world.units.other.ANY_BABY - -- create a new table with the units that aren't being removed in this iteration - for _, v in ipairs(babyUnits) do - if not v.id == unit.id then - table.insert(leftoverUnits, v) - end + local idx = utils.linear_index(ANY_BABY, unit.id, 'id') + if idx then + ANY_BABY:erase(idx) end - -- create a shifted table of the leftover units to make up for lua tables starting with index 1 and the game starting with index 0 - for i = 0, #leftoverUnits - 1, 1 do - local x = i+1 - shiftedLeftoverUnits[i] = leftoverUnits[x] - end - -- copy the leftover units back to the game table - df.global.world.units.other.ANY_BABY = shiftedLeftoverUnits - -- set extra flags to defaults unit.flags1.rider = false unit.relationship_ids.RiderMount = -1 - unit.mount_type = 0 + unit.mount_type = df.rider_positions_type.STANDARD unit.profession2 = df.profession.STANDARD - unit.idle_area_type = 26 + unit.idle_area_type = df.unit_station_type.MillBuilding unit.mood = -1 -- let the mom know she isn't carrying anyone anymore - local motherUnitId = unit.relationship_ids.Mother - df.unit.find(motherUnitId).flags1.ridden = false + local mother = df.unit.find(unit.relationship_ids.Mother) + if mother then mother.flags1.ridden = false end end unit.profession = df.profession.STANDARD + unit.profession2 = df.profession.STANDARD + if hf then hf.profession = df.profession.STANDARD end + end + if not quiet then + print(name .. ' is now ' .. age .. ' years old and will live to at least 160') end - print(name .. ' is now ' .. age .. ' years old and will live to at least 160') end -function main(args) +local function main(args) local units = {} --as:df.unit[] if args.all then units = dfhack.units.getCitizens() @@ -67,7 +62,7 @@ function main(args) table.insert(units, dfhack.gui.getSelectedUnit(true) or qerror("Please select a unit in the UI.")) end for _, u in ipairs(units) do - rejuvenate(u, args.force, args['dry-run'], args.age) + rejuvenate(u, false, args.force, args['dry-run'], args.age) end end From f1146d4accb478ecd8b80feaf0863e7603829227 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Sep 2024 10:32:15 -0700 Subject: [PATCH 077/811] reuse rejuvenate logic in armoks-blessing instead of providing an alternate, less complete implementation --- armoks-blessing.lua | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/armoks-blessing.lua b/armoks-blessing.lua index 5f930f985f..9a64c5bc96 100644 --- a/armoks-blessing.lua +++ b/armoks-blessing.lua @@ -1,24 +1,9 @@ -- Adjust all attributes of all dwarves to an ideal -- by vjek +local rejuvenate = reqscript('rejuvenate') local utils = require('utils') -function rejuvenate(unit) - if unit==nil then - print ("No unit available! Aborting with extreme prejudice.") - return - end - - local current_year=df.global.cur_year - local newbirthyear=current_year - 20 - if unit.birth_year < newbirthyear then - unit.birth_year=newbirthyear - end - if unit.old_year < current_year+100 then - unit.old_year=current_year+100 - end - -end -- --------------------------------------------------------------------------- function brainwash_unit(unit) if unit==nil then @@ -251,7 +236,7 @@ function adjust_all_dwarves(skillname) print("Adjusting "..dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(v)))) brainwash_unit(v) elevate_attributes(v) - rejuvenate(v) + rejuvenate.rejuvenate(v, true) if skillname then if df.job_skill_class[skillname] then LegendaryByClass(skillname,v) From f8a4753fe1251dce6177f84af5ff4c75afaf642a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Sep 2024 10:54:45 -0700 Subject: [PATCH 078/811] default to minimum adult age, not a static 20 --- docs/rejuvenate.rst | 20 ++++++++++++-------- rejuvenate.lua | 33 +++++++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 12 deletions(-) diff --git a/docs/rejuvenate.rst b/docs/rejuvenate.rst index 73d6eebf8f..aaa2216ca3 100644 --- a/docs/rejuvenate.rst +++ b/docs/rejuvenate.rst @@ -6,9 +6,9 @@ rejuvenate :tags: fort armok units If your most valuable citizens are getting old, this tool can save them. It -decreases the age of the selected dwarf to 20 years, or to the age specified. -Age can only be increased (e.g. when this tool is run on babies or children) -if the ``--force`` option is specified. +decreases the age of the selected dwarf to the minimum adult age, or to the age +specified. Age can only be increased (e.g. when this tool is run on babies or +children) if the ``--force`` option is specified. Usage ----- @@ -21,11 +21,15 @@ Examples -------- ``rejuvenate`` - Set the age of the selected dwarf to 20 (if they're older). + Set the age of the selected dwarf to 18 (if they're older than 18). The + target age may be different if you have modded dwarves to become an adult + at a different age, or if you have selected a unit that is not a dwarf. ``rejuvenate --all`` - Set the age of all dwarves over 20 to 20. + Set the ages of all adult citizens and residents to their minimum adult + ages. ``rejuvenate --all --force`` - Set the age of all dwarves (including children and babies) to 20. + Set the ages of all citizens and residents (including children and babies) + to their minimum adult ages. ``rejuvenate --age 149 --force`` Set the age of the selected dwarf to 149, even if they are younger. @@ -33,9 +37,9 @@ Options ------- ``--all`` - Rejuvenate all citizens, not just the selected one. + Rejuvenate all citizens and residents instead of a selected unit. ``--age `` - Sets the target to the age specified. If this is not set, the target age defaults to ``20``. + Sets the target to the age specified. If this is not set, the target age defaults to the minimum adult age for the unit. ``--force`` Set age for units under the specified age to the specified age. Useful if there are too many babies around... diff --git a/rejuvenate.lua b/rejuvenate.lua index 2ecdc7183e..9952be746f 100644 --- a/rejuvenate.lua +++ b/rejuvenate.lua @@ -2,15 +2,40 @@ local utils = require('utils') +local DEFAULT_CHILD_AGE = 18 +local DEFAULT_OLD_AGE = 160 local ANY_BABY = df.global.world.units.other.ANY_BABY +local function get_caste_misc(unit) + local cre = df.creature_raw.find(unit.race) + if not cre then return end + if unit.caste < 0 or unit.caste >= #cre.caste then + return + end + return cre.caste[unit.caste].misc +end + +local function get_adult_age(misc) + return misc and misc.child_age or DEFAULT_CHILD_AGE +end + +local function get_rand_old_age(misc) + return misc and math.random(misc.maxage_min, misc.maxage_max) or DEFAULT_OLD_AGE +end + -- called by armoks-blessing function rejuvenate(unit, quiet, force, dry_run, age) - age = age or 20 + local name = dfhack.df2console(dfhack.units.getReadableName(unit)) + local misc = get_caste_misc(unit) + local adult_age = get_adult_age(misc) + age = age or adult_age + if age < adult_age then + dfhack.printerr('cannot set age to child or baby range') + return + end local current_year = df.global.cur_year local new_birth_year = current_year - age - local new_old_year = unit.old_year < 0 and -1 or math.max(unit.old_year, new_birth_year + 160) - local name = dfhack.df2console(dfhack.units.getReadableName(unit)) + local new_old_year = unit.old_year < 0 and -1 or math.max(unit.old_year, new_birth_year + get_rand_old_age(misc)) if unit.birth_year > new_birth_year and not force then if not quiet then dfhack.printerr(name .. ' is under ' .. age .. ' years old. Use --force to force.') @@ -50,7 +75,7 @@ function rejuvenate(unit, quiet, force, dry_run, age) if hf then hf.profession = df.profession.STANDARD end end if not quiet then - print(name .. ' is now ' .. age .. ' years old and will live to at least 160') + print(name .. ' is now ' .. age .. ' years old and will live a normal lifespan henceforth') end end From 0471d9ed59d407f188cb8d55b01853402164629d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 8 Sep 2024 14:11:27 -0700 Subject: [PATCH 079/811] use library function for getting the caste raw --- rejuvenate.lua | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/rejuvenate.lua b/rejuvenate.lua index 9952be746f..04c79cdf5f 100644 --- a/rejuvenate.lua +++ b/rejuvenate.lua @@ -7,12 +7,9 @@ local DEFAULT_OLD_AGE = 160 local ANY_BABY = df.global.world.units.other.ANY_BABY local function get_caste_misc(unit) - local cre = df.creature_raw.find(unit.race) - if not cre then return end - if unit.caste < 0 or unit.caste >= #cre.caste then - return - end - return cre.caste[unit.caste].misc + local craw = dfhack.units.getCasteRaw(unit) + if not craw then return end + return craw.misc end local function get_adult_age(misc) From 10abfcbd4f3c0b1e9b58e4aea43baff9cf46c8bd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 8 Sep 2024 19:16:47 -0700 Subject: [PATCH 080/811] add race filter on histfig page and refactor exportlegends overlays into separate files and fix missed export start when Export XML button is clicked but the user has already navigated to a subpage --- changelog.txt | 1 + exportlegends.lua | 123 ++++------------------- internal/exportlegends/asyncexport.lua | 104 ++++++++++++++++++++ internal/exportlegends/racefilter.lua | 130 +++++++++++++++++++++++++ 4 files changed, 255 insertions(+), 103 deletions(-) create mode 100644 internal/exportlegends/asyncexport.lua create mode 100644 internal/exportlegends/racefilter.lua diff --git a/changelog.txt b/changelog.txt index 482ed94054..d4b6641e58 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,6 +37,7 @@ Template for new versions: - `caravan`: If you have managed to select an item that is ethically unacceptable to the merchant, an "Ethics warning" badge will now appear next to the "Trade" button. Clicking on the badge will show you which items that you have selected are problematic. The dialog has a button that you can click to deselect the problematic items in the trade list. - `confirm`: If you have ethically unacceptable items selected for trade, the "Are you sure you want to trade" confirmation will warn you about them - `quickfort`: ``#zone`` blueprints now integrated with `preserve-rooms` so you can create a zone and automatically assign it to a noble or administrative role +- `exportlegends`: option to filter by race on historical figures page ## Fixes - `timestream`: ensure child growth events (that is, a child's transition to adulthood) are not skipped; existing "overage" children will be automatically fixed within a year diff --git a/exportlegends.lua b/exportlegends.lua index 131e415676..80a4ec2996 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -2,10 +2,11 @@ --luacheck-flags: strictsubtype --@ module=true -local gui = require('gui') -local overlay = require('plugins.overlay') +local asyncexport = reqscript('internal/exportlegends/asyncexport') +local racefilter = reqscript('internal/exportlegends/racefilter') local script = require('gui.script') -local widgets = require('gui.widgets') + +local GLOBAL_KEY = 'exportlegends' -- Get the date of the world as a string -- Format: "YYYYY-MM-DD" @@ -42,10 +43,7 @@ local function table_containskey(self, key) return false end -progress_item = progress_item or '' -num_done = num_done or -1 -num_total = num_total or -1 -last_update_ms = 0 +local last_update_ms = 0 -- should be frequent enough so that user can still effectively use -- the vanilla legends UI to browse while export is in progress @@ -62,12 +60,12 @@ end --luacheck: skip local function progress_ipairs(vector, desc, skip_count, interval) desc = desc or 'item' - progress_item = desc + asyncexport.progress_item = desc interval = interval or 10000 local cb = ipairs(vector) return function(vector, k, ...) if not skip_count then - num_done = num_done + 1 + asyncexport.num_done = asyncexport.num_done + 1 end if k then if #vector >= interval and (k % interval == 0 or k == #vector - 1) then @@ -80,7 +78,7 @@ local function progress_ipairs(vector, desc, skip_count, interval) end local function make_chunk(name, vector, fn) - num_total = num_total + #vector + asyncexport.num_total = asyncexport.num_total + #vector return { name=name, vector=vector, @@ -1016,113 +1014,32 @@ local function export_more_legends_xml() end local function wrap_export() - if num_total >= 0 then + if asyncexport.num_total >= 0 then qerror('exportlegends already in progress') end - num_total = 0 - num_done = 0 - progress_item = 'basic info' + asyncexport.num_total = 0 + asyncexport.num_done = 0 + asyncexport.progress_item = 'basic info' yield_if_timeout() local ok, err = pcall(export_more_legends_xml) if not ok then dfhack.printerr(err) end - num_total = -1 - num_done = -1 - progress_item = '' -end - --- ------------------- --- LegendsOverlay --- - -LegendsOverlay = defclass(LegendsOverlay, overlay.OverlayWidget) -LegendsOverlay.ATTRS{ - desc='Adds extended export progress bar to the legends main screen.', - default_pos={x=2, y=2}, - default_enabled=true, - viewscreens='legends/Default', - frame={w=55, h=5}, -} - -function LegendsOverlay:init() - self:addviews{ - widgets.Panel{ - view_id='button_mask', - frame={t=0, l=0, w=15, h=3}, - }, - widgets.BannerPanel{ - frame={b=0, l=0, r=0, h=1}, - subviews={ - widgets.ToggleHotkeyLabel{ - view_id='do_export', - frame={t=0, l=1, r=1}, - label='Also export DFHack extended legends data:', - key='CUSTOM_CTRL_D', - visible=function() return num_total < 0 end, - }, - widgets.Label{ - frame={t=0, l=1}, - text={ - 'Exporting ', - {width=27, text=function() return progress_item end}, - ' ', - {text=function() return ('%.2f'):format((num_done * 100) / num_total) end, pen=COLOR_YELLOW}, - '% complete' - }, - visible=function() return num_total >= 0 end, - }, - }, - }, - } -end - -function LegendsOverlay:onInput(keys) - if keys._MOUSE_L and num_total < 0 and - self.subviews.button_mask:getMousePos() and - self.subviews.do_export:getOptionValue() - then - script.start(wrap_export) - end - return LegendsOverlay.super.onInput(self, keys) + asyncexport.reset_state() end --- ------------------- --- DoneMaskOverlay --- - -DoneMaskOverlay = defclass(DoneMaskOverlay, overlay.OverlayWidget) -DoneMaskOverlay.ATTRS{ - desc='Prevents legends mode from being exited while an export is in progress.', - default_pos={x=-2, y=2}, - default_enabled=true, - viewscreens='legends', - frame={w=9, h=3}, +OVERLAY_WIDGETS = { + export=asyncexport.LegendsOverlay, + mask=asyncexport.DoneMaskOverlay, + histfigfilter=racefilter.RaceFilterOverlay, } -function DoneMaskOverlay:init() - self:addviews{ - widgets.Panel{ - frame_background=gui.CLEAR_PEN, - visible=function() return num_total >= 0 end, - } - } -end - -function DoneMaskOverlay:onInput(keys) - if num_total >= 0 then - if keys.LEAVESCREEN or (keys._MOUSE_L and self:getMousePos()) then - return true - end +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_VIEWSCREEN_CHANGED and df.viewscreen_choose_game_typest:is_instance(dfhack.gui.getDFViewscreen(true)) then + asyncexport.reset_state() end - return DoneMaskOverlay.super.onInput(self, keys) end -OVERLAY_WIDGETS = { - export=LegendsOverlay, - mask=DoneMaskOverlay, -} - if dfhack_flags.module then return end diff --git a/internal/exportlegends/asyncexport.lua b/internal/exportlegends/asyncexport.lua new file mode 100644 index 0000000000..21c4bb95fd --- /dev/null +++ b/internal/exportlegends/asyncexport.lua @@ -0,0 +1,104 @@ +--@module = true + +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +progress_item = nil +num_done = nil +num_total = nil + +function reset_state() + progress_item = '' + num_done = -1 + num_total = -1 +end +reset_state() + +-- ------------------- +-- LegendsOverlay +-- + +LegendsOverlay = defclass(LegendsOverlay, overlay.OverlayWidget) +LegendsOverlay.ATTRS{ + desc='Adds extended export progress bar to the legends main screen.', + default_pos={x=2, y=2}, + default_enabled=true, + viewscreens='legends', + frame={w=55, h=5}, +} + +function LegendsOverlay:init() + self:addviews{ + widgets.Panel{ + view_id='button_mask', + frame={t=0, l=0, w=15, h=3}, + }, + widgets.BannerPanel{ + frame={b=0, l=0, r=0, h=1}, + visible=function() return dfhack.gui.matchFocusString('legends/Default', dfhack.gui.getDFViewscreen(true)) end, + subviews={ + widgets.ToggleHotkeyLabel{ + view_id='do_export', + frame={t=0, l=1, r=1}, + label='Also export DFHack extended legends data:', + key='CUSTOM_CTRL_D', + visible=function() return num_total < 0 end, + }, + widgets.Label{ + frame={t=0, l=1}, + text={ + 'Exporting ', + {width=27, text=function() return progress_item end}, + ' ', + {text=function() return ('%.2f'):format((num_done * 100) / num_total) end, pen=COLOR_YELLOW}, + '% complete' + }, + visible=function() return num_total >= 0 end, + }, + }, + }, + } +end + +function LegendsOverlay:onInput(keys) + if keys._MOUSE_L and self.subviews.button_mask:getMousePos() and self.subviews.do_export:getOptionValue() then + if num_total < 0 then + dfhack.run_script('exportlegends') + else + return true + end + end + return LegendsOverlay.super.onInput(self, keys) +end + +-- ------------------- +-- DoneMaskOverlay +-- + +DoneMaskOverlay = defclass(DoneMaskOverlay, overlay.OverlayWidget) +DoneMaskOverlay.ATTRS{ + desc='Prevents legends mode from being exited while an export is in progress.', + default_pos={x=-2, y=2}, + default_enabled=true, + viewscreens='legends', + frame={w=9, h=3}, +} + +function DoneMaskOverlay:init() + self:addviews{ + widgets.Panel{ + frame_background=gui.CLEAR_PEN, + visible=function() return num_total >= 0 end, + } + } +end + +function DoneMaskOverlay:onInput(keys) + if num_total >= 0 then + if keys.LEAVESCREEN or (keys._MOUSE_L and self:getMousePos()) then + return true + end + end + return DoneMaskOverlay.super.onInput(self, keys) +end diff --git a/internal/exportlegends/racefilter.lua b/internal/exportlegends/racefilter.lua new file mode 100644 index 0000000000..52524a9b71 --- /dev/null +++ b/internal/exportlegends/racefilter.lua @@ -0,0 +1,130 @@ +--@module = true + +local dlg = require('gui.dialogs') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +local choices, race_to_label, hfid_to_race, hfid_to_name, cur_race, prev_search + +function reset_state() + choices = {} + race_to_label = {[-1]='All'} + hfid_to_race = {} + hfid_to_name = {} + cur_race = -1 + prev_search = '' +end +reset_state() + +-- ------------------- +-- RaceFilterOverlay +-- + +RaceFilterOverlay = defclass(RaceFilterOverlay, overlay.OverlayWidget) +RaceFilterOverlay.ATTRS { + desc="Adds the ability to filter historical figures by race in legends mode.", + default_pos={x=56, y=11}, + default_enabled=true, + viewscreens='legends', -- finer grained visibility managed in render and onInput functions + frame={w=54, h=1}, -- can't use visible property due to self.dirty state management +} + +function RaceFilterOverlay:init() + self:addviews{ + widgets.BannerPanel{ + subviews={ + widgets.HotkeyLabel{ + frame={l=1}, + label='Filter by race:', + key='CUSTOM_ALT_S', + auto_width=true, + on_activate=self:callback('choose_race'), + }, + widgets.Label{ + frame={l=24}, + text={{text=function() return race_to_label[cur_race] end}}, + text_pen=COLOR_YELLOW, + }, + }, + }, + } +end + +function RaceFilterOverlay:set_race(_, choice) + if cur_race == choice.race then return end + cur_race = choice.race + self.dirty = true +end + +function RaceFilterOverlay:choose_race() + if #choices == 0 then + for race,cre in ipairs(df.global.world.raws.creatures.all) do + local label = string.lower(cre.creature_id) + race_to_label[race] = label + table.insert(choices, {text=label, race=race}) + end + table.sort(choices, function(a, b) return a.text < b.text end) + table.insert(choices, 1, {text='All', race=-1}) + end + + dlg.showListPrompt('Races', 'Choose race filter', COLOR_WHITE, choices, + self:callback('set_race'), nil, 30, true) +end + +local function do_filter(scr, filter_str, full_refresh) + print('filtering', cur_race, filter_str, full_refresh) + if full_refresh then + scr.histfigs_filtered:resize(#scr.histfigs) + for i=0,#scr.histfigs-1 do + scr.histfigs_filtered[i] = i + end + end + local filter_by_name = full_refresh and #filter_str > 0 + filter_str = dfhack.toSearchNormalized(filter_str) + if cur_race < 0 and not filter_by_name then return end + for idx=#scr.histfigs_filtered-1,0,-1 do + local hfid = scr.histfigs[scr.histfigs_filtered[idx]] + if not hfid_to_race[hfid] then + local hf = df.historical_figure.find(hfid) + hfid_to_race[hfid] = hf and hf.race or -1 + hfid_to_name[hfid] = hf and + dfhack.toSearchNormalized( + ('%s %s'):format(dfhack.TranslateName(hf.name, false), dfhack.TranslateName(hf.name, true))) or '' + end + if cur_race >= 0 and hfid_to_race[hfid] ~= cur_race then + scr.histfigs_filtered:erase(idx) + elseif filter_by_name and not hfid_to_name[hfid]:match(filter_str) then + scr.histfigs_filtered:erase(idx) + end + end +end + +local function get_cur_page(scr) + scr = scr or dfhack.gui.getDFViewscreen(true) + return scr.page[scr.active_page_index] +end + +local function is_hf_page(scr, page) + page = page or get_cur_page(scr) + return page.mode == df.legend_pagest.T_mode.HFS and page.index == -1 +end + +function RaceFilterOverlay:render(dc) + local scr = dfhack.gui.getDFViewscreen(true) + local page = get_cur_page(scr) + if not is_hf_page(scr, page) then + self.dirty = true + return + end + if self.dirty or prev_search ~= page.filter_str then + do_filter(scr, page.filter_str, self.dirty) + prev_search = page.filter_str + self.dirty = false + end + RaceFilterOverlay.super.render(self, dc) +end + +function RaceFilterOverlay:onInput(keys) + if not is_hf_page() then return end + RaceFilterOverlay.super.onInput(self, keys) +end From 67d7c13b8ba08185504544f96fff5208a0373b7e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 8 Sep 2024 19:21:17 -0700 Subject: [PATCH 081/811] update docs --- docs/exportlegends.rst | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/docs/exportlegends.rst b/docs/exportlegends.rst index 2d7c4702d6..5d262a853c 100644 --- a/docs/exportlegends.rst +++ b/docs/exportlegends.rst @@ -11,7 +11,10 @@ about your world so that it can be browsed with external programs like get with vanilla export functionality, and many external tools depend on this extra information. -By default, ``exportlegends`` hooks into the standard vanilla ``Export XML`` button and runs in the background when you click it, allowing both the vanilla export and the extended data export to execute simultaneously. You can continue to browse legends mode via the vanilla UI while the export is running. +By default, ``exportlegends`` hooks into the standard vanilla ``Export XML`` +button and runs in the background when you click it, allowing both the vanilla +export and the extended data export to execute simultaneously. You can continue +to browse legends mode via the vanilla UI while the export is running. To use: @@ -35,7 +38,11 @@ Usage Overlay ------- -This script also provides an overlay that is managed by the `overlay` framework. +This script also provides several overlays that are managed by the `overlay` +framework. + +**exportlegends.export** + When the overlay is enabled, a toggle for exporting extended legends data will appear below the vanilla "Export XML" button. If the toggle is enabled when the "Export XML" button is clicked, then ``exportlegends`` will run alongside the @@ -45,6 +52,13 @@ While the extended data is being exported, a status line will appear in place of the toggle, reporting the current export target and the overall percent complete. -There is an additional overlay that masks out the "Done" button while the -extended export is running. This prevents the player from exiting legends mode -before the export is complete. +**exportlegends.mask** + +This overlay masks out the "Done" button while the extended export is running. +This prevents the player from accidentally exiting legends mode before the +export is complete. + +**exportlegends.histfigfilter** + +This overlay adds a filter widget to the Historical Figures legends page. +Clicking the widget allows you to filter the list of historical figures by race. From 976d3e69a0b4e7c93d5caee45c64788d12aca7b7 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Mon, 9 Sep 2024 13:19:42 +0200 Subject: [PATCH 082/811] hide panel when assigning someone to the workshop --- idle-crafting.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/idle-crafting.lua b/idle-crafting.lua index 4f74ae7249..b618a0a67f 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -4,6 +4,7 @@ local overlay = require('plugins.overlay') local widgets = require('gui.widgets') local repeatutil = require("repeat-util") +local orders = require('plugins.orders') ---create a new linked job ---@return df.job @@ -361,6 +362,7 @@ IdleCraftingOverlay.ATTRS { 'dwarfmode/ViewSheets/BUILDING/Workshop/Craftsdwarfs/Workers', }, frame = { w = 54, h = 1 }, + visible = orders.can_set_labors } function IdleCraftingOverlay:init() From 3f3514a211f928791d38a05949d2926915cf813c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 4 Sep 2024 23:30:53 +0200 Subject: [PATCH 083/811] Add basic gui/notes tool print notes list and centering on them --- gui/notes.lua | 125 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 gui/notes.lua diff --git a/gui/notes.lua b/gui/notes.lua new file mode 100644 index 0000000000..03794b5bb2 --- /dev/null +++ b/gui/notes.lua @@ -0,0 +1,125 @@ +-- Map notes +--@ module = true + +local gui = require 'gui' +local widgets = require 'gui.widgets' +local script = require 'gui.script' +local text_editor = reqscript('internal/journal/text_editor') + +local map_points = df.global.plotinfo.waypoints.points + +local NOTE_LIST_RESIZE_MIN = {w=26} +local RESIZE_MIN = {w=65, h=30} +local NOTE_SEARCH_BATCH_SIZE = 10 + +NotesWindow = defclass(NotesWindow, widgets.Window) +NotesWindow.ATTRS { + frame_title='DF Notes', + resizable=true, + resize_min=RESIZE_MIN, + frame_inset={l=0,r=0,t=0,b=0}, +} + +function NotesWindow:init() + self:addviews{ + widgets.Panel{ + view_id='note_list_panel', + frame={l=0, w=NOTE_LIST_RESIZE_MIN.w, t=0, b=1}, + visible=true, + frame_inset={l=1, t=1, b=1, r=1}, + autoarrange_subviews=true, + + subviews={ + widgets.HotkeyLabel { + key='CUSTOM_ALT_S', + label='Search', + frame={l=0}, + auto_width=true, + on_activate=function() self.subviews.search:setFocus(true) end, + }, + text_editor.TextEditor{ + view_id='search', + frame={l=0,h=3}, + frame_style=gui.FRAME_INTERIOR, + one_line_mode=true, + on_text_change=self:callback('loadFilteredNotes') + }, + widgets.List{ + view_id='note_list', + frame={l=0}, + frame_inset={t=1}, + row_height=1, + on_submit=self:callback('loadNote') + }, + } + }, + widgets.Divider{ + view_id='note_list_divider', + + frame={l=NOTE_LIST_RESIZE_MIN.w,t=0,b=0,w=1}, + + interior_b=false, + frame_style_t=false, + frame_style_b=false, + }, + } + + self:loadFilteredNotes('') +end + +function NotesWindow:loadNote(ind, note) + dfhack.gui.pauseRecenter(note.point.pos) +end + +function NotesWindow:loadFilteredNotes(search_phrase) + script.start(function () + local choices = {} + + for ind, map_point in ipairs(map_points) do + if ind > 0 and ind % NOTE_SEARCH_BATCH_SIZE == 0 then + script.sleep(1, 'frames') + end + + if #search_phrase < 3 or map_point.name:find(search_phrase) then + table.insert(choices, { + text=map_point.name, + point=map_point + }) + end + end + + self.subviews.note_list:setChoices(choices) + end) +end + + +NotesScreen = defclass(NotesScreen, gui.ZScreen) +NotesScreen.ATTRS { + focus_path='gui/notes', +} + +function NotesScreen:init() + self:addviews{ + NotesWindow{ + view_id='notes_window', + frame={w=RESIZE_MIN.w, h=35}, + }, + } +end + +function NotesScreen:onDismiss() + view = nil +end + +function main(options) + if not dfhack.isMapLoaded() or not dfhack.world.isFortressMode() then + qerror('notes requires a fortress map to be loaded') + end + + view = view and view:raise() or NotesScreen{ + }:show() +end + +if not dfhack_flags.module then + main() +end From 9f10c5de88c41c3ababd8fd796e83638aa72a254 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 5 Sep 2024 07:53:41 +0200 Subject: [PATCH 084/811] Improve gui notes searching engine logic --- gui/notes.lua | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 03794b5bb2..2a14613a52 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -10,7 +10,7 @@ local map_points = df.global.plotinfo.waypoints.points local NOTE_LIST_RESIZE_MIN = {w=26} local RESIZE_MIN = {w=65, h=30} -local NOTE_SEARCH_BATCH_SIZE = 10 +local NOTE_SEARCH_BATCH_SIZE = 25 NotesWindow = defclass(NotesWindow, widgets.Window) NotesWindow.ATTRS { @@ -72,15 +72,36 @@ function NotesWindow:loadNote(ind, note) end function NotesWindow:loadFilteredNotes(search_phrase) + local full_list_loaded = self.curr_search_phrase == '' + + search_phrase = search_phrase:lower() + if #search_phrase < 3 then + search_phrase = '' + end + + self.curr_search_phrase = search_phrase + script.start(function () + if #search_phrase == 0 and full_list_loaded then + return + end + local choices = {} for ind, map_point in ipairs(map_points) do if ind > 0 and ind % NOTE_SEARCH_BATCH_SIZE == 0 then script.sleep(1, 'frames') end + if self.curr_search_phrase ~= search_phrase then + -- stop the work if user provided new search phrase + return + end - if #search_phrase < 3 or map_point.name:find(search_phrase) then + local point_name_lowercase = map_point.name:lower() + if ( + point_name_lowercase ~= nil and #point_name_lowercase > 0 and + point_name_lowercase:find(search_phrase) + ) then table.insert(choices, { text=map_point.name, point=map_point From df9c87b3c046e084c4cc6c5d9dc1349a0c2f6609 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 8 Sep 2024 19:44:39 +0200 Subject: [PATCH 085/811] Prepare size-adjustable note details view for gui/notes --- gui/notes.lua | 82 +++++++++++++++++++++++++++++++++++++++++++++++++-- notes.lua | 2 +- 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 2a14613a52..6d06efc0e8 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -21,6 +21,8 @@ NotesWindow.ATTRS { } function NotesWindow:init() + self.selected_note = nil + self:addviews{ widgets.Panel{ view_id='note_list_panel', @@ -49,7 +51,7 @@ function NotesWindow:init() frame={l=0}, frame_inset={t=1}, row_height=1, - on_submit=self:callback('loadNote') + on_submit=function (ind, note) self:loadNote(note) end }, } }, @@ -62,12 +64,80 @@ function NotesWindow:init() frame_style_t=false, frame_style_b=false, }, + widgets.Panel{ + view_id='note_details', + frame={l=NOTE_LIST_RESIZE_MIN.w + 1,t=0,b=0}, + frame_inset=1, + autoarrange_gap=1, + subviews={ + widgets.Panel{ + view_id="name_panel", + frame_title='Name', + frame_style=gui.FRAME_INTERIOR, + frame={l=0,r=0,t=0,h=4}, + frame_inset={l=1,r=1}, + auto_height=true, + subviews={ + widgets.Label{ + view_id='name', + frame={t=0,l=0,r=0} + }, + }, + }, + widgets.Panel{ + view_id="comment_panel", + frame_title='Comment', + frame_style=gui.FRAME_INTERIOR, + frame={l=0,r=0,t=4,b=2}, + frame_inset={l=1,r=1,t=1}, + subviews={ + widgets.Label{ + view_id='comment', + frame={t=0,l=0,r=0} + }, + } + }, + widgets.Panel{ + frame={l=0,r=0,b=0,h=2}, + frame_inset={l=1,r=1,t=1}, + subviews={ + widgets.HotkeyLabel{ + view_id='edit', + frame={l=0,t=0,h=1}, + auto_width=true, + label='Edit', + key='CUSTOM_ALT_U', + -- on_activate=function() self:createNote() end, + -- enabled=function() return #self.subviews.name:getText() > 0 end, + }, + widgets.HotkeyLabel{ + view_id='delete', + frame={r=0,t=0,h=1}, + auto_width=true, + label='Delete', + key='CUSTOM_ALT_D', + -- on_activate=function() self:deleteNote() end, + }, + } + } + } + } } self:loadFilteredNotes('') end -function NotesWindow:loadNote(ind, note) +function NotesWindow:loadNote(note) + self.selected_note = note + + local note_width = self.subviews.name_panel.frame_body.width + local wrapped_name = note.point.name:wrap(note_width) + local wrapped_comment = note.point.comment:wrap(note_width) + + self.subviews.name:setText(wrapped_name) + self.subviews.comment:setText(wrapped_comment) + self.subviews.note_details:updateLayout() + dfhack.gui.pauseRecenter(note.point.pos) end @@ -113,6 +183,14 @@ function NotesWindow:loadFilteredNotes(search_phrase) end) end +function NotesWindow:postUpdateLayout() + if self.selected_note == nil then + self.subviews.note_list:submit() + else + self:loadNote(self.selected_note) + end +end + NotesScreen = defclass(NotesScreen, gui.ZScreen) NotesScreen.ATTRS { diff --git a/notes.lua b/notes.lua index 99aab4a748..8b71a07643 100644 --- a/notes.lua +++ b/notes.lua @@ -227,7 +227,7 @@ function NoteManager:init() key='CUSTOM_ALT_D', visible=edit_mode, on_activate=function() self:deleteNote() end, - } or nil, + }, } } }, From f480dde05ad854293df01a0d3d195bc7830fe03b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 8 Sep 2024 20:28:30 +0200 Subject: [PATCH 086/811] Add a way to update/delete note from the `gui/notes` tool --- gui/notes.lua | 51 ++++++--- internal/notes/note_manager.lua | 180 ++++++++++++++++++++++++++++++++ notes.lua | 173 +----------------------------- 3 files changed, 219 insertions(+), 185 deletions(-) create mode 100644 internal/notes/note_manager.lua diff --git a/gui/notes.lua b/gui/notes.lua index 6d06efc0e8..c6e8e290f5 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -5,6 +5,7 @@ local gui = require 'gui' local widgets = require 'gui.widgets' local script = require 'gui.script' local text_editor = reqscript('internal/journal/text_editor') +local note_manager = reqscript('internal/notes/note_manager') local map_points = df.global.plotinfo.waypoints.points @@ -22,6 +23,7 @@ NotesWindow.ATTRS { function NotesWindow:init() self.selected_note = nil + self.note_manager = nil self:addviews{ widgets.Panel{ @@ -107,8 +109,7 @@ function NotesWindow:init() auto_width=true, label='Edit', key='CUSTOM_ALT_U', - -- on_activate=function() self:createNote() end, - -- enabled=function() return #self.subviews.name:getText() > 0 end, + on_activate=function() self:showNoteManager(self.selected_note) end, }, widgets.HotkeyLabel{ view_id='delete', @@ -116,7 +117,7 @@ function NotesWindow:init() auto_width=true, label='Delete', key='CUSTOM_ALT_D', - -- on_activate=function() self:deleteNote() end, + on_activate=function() self:deleteNote(self.selected_note) end, }, } } @@ -124,12 +125,41 @@ function NotesWindow:init() } } - self:loadFilteredNotes('') + self:loadFilteredNotes('', true) +end + +function NotesWindow:showNoteManager(note) + if self.note_manager ~= nil then + self.note_manager:dismiss() + end + + self.note_manager = note_manager.NoteManager{ + note=note, + on_dismiss=function() self.visible = true end + } + + self.visible = false + return self.note_manager:show():raise() +end + +function NotesWindow:deleteNote(note) + for ind, map_point in pairs(map_points) do + if map_point.id == note.point.id then + map_points:erase(ind) + break + end + end + + self:loadFilteredNotes(self.curr_search_phrase, true) end function NotesWindow:loadNote(note) self.selected_note = note + if note == nil then + return + end + local note_width = self.subviews.name_panel.frame_body.width local wrapped_name = note.point.name:wrap(note_width) local wrapped_comment = note.point.comment:wrap(note_width) @@ -141,7 +171,7 @@ function NotesWindow:loadNote(note) dfhack.gui.pauseRecenter(note.point.pos) end -function NotesWindow:loadFilteredNotes(search_phrase) +function NotesWindow:loadFilteredNotes(search_phrase, force) local full_list_loaded = self.curr_search_phrase == '' search_phrase = search_phrase:lower() @@ -152,7 +182,7 @@ function NotesWindow:loadFilteredNotes(search_phrase) self.curr_search_phrase = search_phrase script.start(function () - if #search_phrase == 0 and full_list_loaded then + if #search_phrase == 0 and full_list_loaded and not force then return end @@ -180,15 +210,8 @@ function NotesWindow:loadFilteredNotes(search_phrase) end self.subviews.note_list:setChoices(choices) - end) -end - -function NotesWindow:postUpdateLayout() - if self.selected_note == nil then self.subviews.note_list:submit() - else - self:loadNote(self.selected_note) - end + end) end diff --git a/internal/notes/note_manager.lua b/internal/notes/note_manager.lua new file mode 100644 index 0000000000..79c76b4e7f --- /dev/null +++ b/internal/notes/note_manager.lua @@ -0,0 +1,180 @@ +--@ module = true + +local gui = require('gui') +local widgets = require('gui.widgets') +local text_editor = reqscript('internal/journal/text_editor') + +local waypoints = df.global.plotinfo.waypoints +local map_points = df.global.plotinfo.waypoints.points + +NoteManager = defclass(NoteManager, gui.ZScreen) +NoteManager.ATTRS{ + focus_path='notes/note-manager', + note=DEFAULT_NIL, + on_update=DEFAULT_NIL, + on_dismiss=DEFAULT_NIL, +} + +function NoteManager:init() + local edit_mode = self.note ~= nil + + self:addviews{ + widgets.Window{ + frame={w=35,h=20}, + frame_inset={t=1}, + resizable=true, + frame_title='Note', + subviews={ + widgets.HotkeyLabel { + key='CUSTOM_ALT_N', + label='Name', + frame={l=0,t=0}, + auto_width=true, + on_activate=function() self.subviews.name:setFocus(true) end, + }, + text_editor.TextEditor{ + view_id='name', + frame={t=1,h=3}, + frame_style=gui.FRAME_INTERIOR, + init_text=self.note and self.note.point.name or '', + -- init_cursor=self.note and #self.note.point.name + 1 or 1, + one_line_mode=true + }, + widgets.HotkeyLabel { + key='CUSTOM_ALT_C', + label='Comment', + frame={l=0,t=5}, + auto_width=true, + on_activate=function() self.subviews.comment:setFocus(true) end, + }, + text_editor.TextEditor{ + view_id='comment', + frame={t=6,b=3}, + frame_style=gui.FRAME_INTERIOR, + init_text=self.note and self.note.point.comment or '', + -- init_cursor=1 + }, + widgets.Panel{ + view_id='buttons', + frame={b=0,h=1}, + frame_inset={l=1,r=1}, + subviews={ + widgets.HotkeyLabel{ + view_id='Save', + frame={l=0,t=0,h=1}, + auto_width=true, + label='Save', + key='CUSTOM_ALT_S', + visible=edit_mode, + on_activate=function() self:saveNote() end, + enabled=function() return #self.subviews.name:getText() > 0 end, + }, + widgets.HotkeyLabel{ + view_id='Create', + frame={l=0,t=0,h=1}, + auto_width=true, + label='Create', + key='CUSTOM_ALT_S', + visible=not edit_mode, + on_activate=function() self:createNote() end, + enabled=function() return #self.subviews.name:getText() > 0 end, + }, + widgets.HotkeyLabel{ + view_id='delete', + frame={r=0,t=0,h=1}, + auto_width=true, + label='Delete', + key='CUSTOM_ALT_D', + visible=edit_mode, + on_activate=function() self:deleteNote() end, + }, + } + } + }, + }, + } +end + +function NoteManager:createNote() + local cursor_pos = guidm.getCursorPos() + if cursor_pos == nil then + dfhack.printerr('Enable keyboard cursor to add a note.') + return + end + + local name = self.subviews.name:getText() + local comment = self.subviews.comment:getText() + + if #name == 0 then + dfhack.printerr('Note need at least a name') + return + end + + map_points:insert("#", { + new=true, + + id = waypoints.next_point_id, + tile=88, + fg_color=7, + bg_color=0, + name=name, + comment=comment, + pos=cursor_pos + }) + waypoints.next_point_id = waypoints.next_point_id + 1 + + if self.on_update then + self.on_update() + end + + self:dismiss() +end + +function NoteManager:saveNote() + if self.note == nil then + return + end + + local name = self.subviews.name:getText() + local comment = self.subviews.comment:getText() + + if #name == 0 then + dfhack.printerr('Note need at least a name') + return + end + + self.note.point.name = name + self.note.point.comment = comment + + if self.on_update then + self.on_update() + end + + self:dismiss() +end + +function NoteManager:deleteNote() + if self.note == nil then + return + end + + for ind, map_point in pairs(map_points) do + if map_point.id == self.note.point.id then + map_points:erase(ind) + break + end + end + + if self.on_update then + self.on_update() + end + + self:dismiss() +end + +function NoteManager:onDismiss() + self.note = nil + if self.on_dismiss then + self:on_dismiss() + end +end diff --git a/notes.lua b/notes.lua index 8b71a07643..deef9bfad3 100644 --- a/notes.lua +++ b/notes.lua @@ -5,7 +5,7 @@ local widgets = require('gui.widgets') local textures = require('gui.textures') local overlay = require('plugins.overlay') local guidm = require('gui.dwarfmode') -local text_editor = reqscript('internal/journal/text_editor') +local note_manager = reqscript('internal/notes/note_manager') local green_pin = dfhack.textures.loadTileset( 'hack/data/art/note_green_pin_map.png', @@ -22,7 +22,6 @@ NotesOverlay.ATTRS{ overlay_onupdate_max_freq_seconds=30, } -local waypoints = df.global.plotinfo.waypoints local map_points = df.global.plotinfo.waypoints.points function NotesOverlay:init() @@ -88,7 +87,7 @@ function NotesOverlay:showNoteManager(note) self.note_manager:dismiss() end - self.note_manager = NoteManager{ + self.note_manager = note_manager.NoteManager{ note=note, on_update=function() self:reloadVisibleNotes() end } @@ -148,174 +147,6 @@ function NotesOverlay:reloadVisibleNotes() end end -NoteManager = defclass(NoteManager, gui.ZScreen) -NoteManager.ATTRS{ - focus_path='notes/note-manager', - note=DEFAULT_NIL, - on_update=DEFAULT_NIL, -} - -function NoteManager:init() - local edit_mode = self.note ~= nil - - self:addviews{ - widgets.Window{ - frame={w=35,h=20}, - frame_inset={t=1}, - frame_title='Notes', - resizable=true, - subviews={ - widgets.HotkeyLabel { - key='CUSTOM_ALT_N', - label='Name', - frame={l=0,t=0}, - auto_width=true, - on_activate=function() self.subviews.name:setFocus(true) end, - }, - text_editor.TextEditor{ - view_id='name', - frame={t=1,h=3}, - frame_style=gui.FRAME_INTERIOR, - init_text=self.note and self.note.point.name or '', - init_cursor=self.note and #self.note.point.name+1 or 1, - one_line_mode=true - }, - widgets.HotkeyLabel { - key='CUSTOM_ALT_C', - label='Comment', - frame={l=0,t=5}, - auto_width=true, - on_activate=function() self.subviews.comment:setFocus(true) end, - }, - text_editor.TextEditor{ - view_id='comment', - frame={t=6,b=3}, - frame_style=gui.FRAME_INTERIOR, - init_text=self.note and self.note.point.comment or '', - init_cursor=1 - }, - widgets.Panel{ - view_id='buttons', - frame={b=0,h=1}, - frame_inset={l=1,r=1}, - subviews={ - widgets.HotkeyLabel{ - view_id='Save', - frame={l=0,t=0,h=1}, - auto_width=true, - label='Save', - key='CUSTOM_ALT_S', - visible=edit_mode, - on_activate=function() self:saveNote() end, - enabled=function() return #self.subviews.name:getText() > 0 end, - }, - widgets.HotkeyLabel{ - view_id='Create', - frame={l=0,t=0,h=1}, - auto_width=true, - label='Create', - key='CUSTOM_ALT_S', - visible=not edit_mode, - on_activate=function() self:createNote() end, - enabled=function() return #self.subviews.name:getText() > 0 end, - }, - widgets.HotkeyLabel{ - view_id='delete', - frame={r=0,t=0,h=1}, - auto_width=true, - label='Delete', - key='CUSTOM_ALT_D', - visible=edit_mode, - on_activate=function() self:deleteNote() end, - }, - } - } - }, - }, - } -end - -function NoteManager:createNote() - local cursor_pos = guidm.getCursorPos() - if cursor_pos == nil then - dfhack.printerr('Enable keyboard cursor to add a note.') - return - end - - local name = self.subviews.name:getText() - local comment = self.subviews.comment:getText() - - if #name == 0 then - dfhack.printerr('Note need at least a name') - return - end - - map_points:insert("#", { - new=true, - - id = waypoints.next_point_id, - tile=88, - fg_color=7, - bg_color=0, - name=name, - comment=comment, - pos=cursor_pos - }) - waypoints.next_point_id = waypoints.next_point_id + 1 - - if self.on_update then - self.on_update() - end - - self:dismiss() -end - -function NoteManager:saveNote() - if self.note == nil then - return - end - - local name = self.subviews.name:getText() - local comment = self.subviews.comment:getText() - - if #name == 0 then - dfhack.printerr('Note need at least a name') - return - end - - self.note.point.name = name - self.note.point.comment = comment - - if self.on_update then - self.on_update() - end - - self:dismiss() -end - -function NoteManager:deleteNote() - if self.note == nil then - return - end - - for ind, map_point in pairs(map_points) do - if map_point.id == self.note.point.id then - map_points:erase(ind) - break - end - end - - if self.on_update then - self.on_update() - end - - self:dismiss() -end - -function NoteManager:onDismiss() - self.note = nil -end - -- register widgets OVERLAY_WIDGETS = { map_notes=NotesOverlay From 47a22707a0a8c440aa10860e1e43e6baf1bddf20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 8 Sep 2024 20:50:39 +0200 Subject: [PATCH 087/811] Make gui/notes center on note on submit --- gui/notes.lua | 49 +++++++++++++++++++++++--------- internal/journal/text_editor.lua | 10 ++++++- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index c6e8e290f5..da52966eef 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -32,7 +32,6 @@ function NotesWindow:init() visible=true, frame_inset={l=1, t=1, b=1, r=1}, autoarrange_subviews=true, - subviews={ widgets.HotkeyLabel { key='CUSTOM_ALT_S', @@ -46,14 +45,35 @@ function NotesWindow:init() frame={l=0,h=3}, frame_style=gui.FRAME_INTERIOR, one_line_mode=true, - on_text_change=self:callback('loadFilteredNotes') + on_text_change=self:callback('loadFilteredNotes'), + on_submit=function() + self.subviews.note_list:submit() + end + }, + widgets.Panel{ + frame={h=2}, + frame_inset={t=1}, + subviews={ + widgets.HotkeyLabel { + key='CUSTOM_ALT_L', + label='Notes', + frame={l=0,t=0}, + auto_width=true, + on_activate=function() + self.subviews.note_list:setFocus(true) + end, + }, + } }, widgets.List{ view_id='note_list', frame={l=0}, - frame_inset={t=1}, + frame_inset={t=0}, row_height=1, - on_submit=function (ind, note) self:loadNote(note) end + on_submit=function (ind, note) + self:loadNote(note) + dfhack.gui.pauseRecenter(note.point.pos) + end }, } }, @@ -160,15 +180,16 @@ function NotesWindow:loadNote(note) return end - local note_width = self.subviews.name_panel.frame_body.width - local wrapped_name = note.point.name:wrap(note_width) - local wrapped_comment = note.point.comment:wrap(note_width) + local note_details_frame = self.subviews.name_panel.frame_body + if note_details_frame ~= nil then + local note_width = self.subviews.name_panel.frame_body.width + local wrapped_name = note.point.name:wrap(note_width) + local wrapped_comment = note.point.comment:wrap(note_width) - self.subviews.name:setText(wrapped_name) - self.subviews.comment:setText(wrapped_comment) - self.subviews.note_details:updateLayout() - - dfhack.gui.pauseRecenter(note.point.pos) + self.subviews.name:setText(wrapped_name) + self.subviews.comment:setText(wrapped_comment) + self.subviews.note_details:updateLayout() + end end function NotesWindow:loadFilteredNotes(search_phrase, force) @@ -210,7 +231,9 @@ function NotesWindow:loadFilteredNotes(search_phrase, force) end self.subviews.note_list:setChoices(choices) - self.subviews.note_list:submit() + + local sel_ind, sel_note = self.subviews.note_list:getSelected() + self:loadNote(sel_note) end) end diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index cf15776d46..0b73494118 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -94,6 +94,8 @@ TextEditor.ATTRS{ select_pen = COLOR_CYAN, on_text_change = DEFAULT_NIL, on_cursor_change = DEFAULT_NIL, + -- called on submit, only in one line mode + on_submit = DEFAULT_NIL, one_line_mode = false, debug = false } @@ -112,6 +114,7 @@ function TextEditor:init() select_pen=self.select_pen, debug=self.debug, one_line_mode=self.one_line_mode, + on_submit=self.on_submit, on_text_change=function (val) self:updateLayout() @@ -255,6 +258,7 @@ TextEditorView.ATTRS{ enable_cursor_blink = true, debug = false, one_line_mode = false, + on_submit = DEFAULT_NIL, history_size = 10, } @@ -796,7 +800,11 @@ end function TextEditorView:onTextManipulationInput(keys) if keys.SELECT then -- handle enter - if not self.one_line_mode then + if self.one_line_mode then + if self.on_submit then + self:on_submit() + end + else self.history:store( HISTORY_ENTRY.WHITESPACE_BLOCK, self.text, From adebc95df51b88047f49d80bddf362eab7112da1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 8 Sep 2024 22:31:38 +0200 Subject: [PATCH 088/811] Add note add feature to `gui/notes` tool --- gui/notes.lua | 142 +++++++++++++++++++++++++++---- internal/journal/text_editor.lua | 12 ++- internal/notes/note_manager.lua | 10 ++- notes.lua | 2 +- 4 files changed, 144 insertions(+), 22 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index da52966eef..7022b71067 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -3,6 +3,7 @@ local gui = require 'gui' local widgets = require 'gui.widgets' +local guidm = require('gui.dwarfmode') local script = require 'gui.script' local text_editor = reqscript('internal/journal/text_editor') local note_manager = reqscript('internal/notes/note_manager') @@ -13,12 +14,20 @@ local NOTE_LIST_RESIZE_MIN = {w=26} local RESIZE_MIN = {w=65, h=30} local NOTE_SEARCH_BATCH_SIZE = 25 +local green_pin = dfhack.textures.loadTileset( + 'hack/data/art/note_green_pin_map.png', + 32, + 32, + true +) + NotesWindow = defclass(NotesWindow, widgets.Window) NotesWindow.ATTRS { frame_title='DF Notes', resizable=true, resize_min=RESIZE_MIN, frame_inset={l=0,r=0,t=0,b=0}, + on_note_add=DEFAULT_NIL } function NotesWindow:init() @@ -30,7 +39,7 @@ function NotesWindow:init() view_id='note_list_panel', frame={l=0, w=NOTE_LIST_RESIZE_MIN.w, t=0, b=1}, visible=true, - frame_inset={l=1, t=1, b=1, r=1}, + frame_inset={l=1,t=1,b=1,r=1}, autoarrange_subviews=true, subviews={ widgets.HotkeyLabel { @@ -67,7 +76,7 @@ function NotesWindow:init() }, widgets.List{ view_id='note_list', - frame={l=0}, + frame={l=0,b=2}, frame_inset={t=0}, row_height=1, on_submit=function (ind, note) @@ -75,11 +84,22 @@ function NotesWindow:init() dfhack.gui.pauseRecenter(note.point.pos) end }, - } + }, + }, + widgets.HotkeyLabel{ + view_id='create', + frame={l=1,b=1,h=1}, + auto_width=true, + label='New note', + key='CUSTOM_ALT_N', + visible=edit_mode, + on_activate=function() + if self.on_note_add then + self:on_note_add() + end + end, }, widgets.Divider{ - view_id='note_list_divider', - frame={l=NOTE_LIST_RESIZE_MIN.w,t=0,b=0,w=1}, interior_b=false, @@ -155,6 +175,10 @@ function NotesWindow:showNoteManager(note) self.note_manager = note_manager.NoteManager{ note=note, + on_update=function() + self:reloadFilteredNotes() + dfhack.internal.runCommand('overlay trigger notes.map_notes') + end, on_dismiss=function() self.visible = true end } @@ -170,7 +194,7 @@ function NotesWindow:deleteNote(note) end end - self:loadFilteredNotes(self.curr_search_phrase, true) + self:reloadFilteredNotes() end function NotesWindow:loadNote(note) @@ -180,16 +204,26 @@ function NotesWindow:loadNote(note) return end - local note_details_frame = self.subviews.name_panel.frame_body - if note_details_frame ~= nil then - local note_width = self.subviews.name_panel.frame_body.width - local wrapped_name = note.point.name:wrap(note_width) - local wrapped_comment = note.point.comment:wrap(note_width) - - self.subviews.name:setText(wrapped_name) - self.subviews.comment:setText(wrapped_comment) - self.subviews.note_details:updateLayout() + -- self.note_width_calculated = false +end + +function NotesWindow:postUpdateLayout() + if self.selected_note == nil then + return end + local note_details_frame = self.subviews.name_panel.frame_body + + local note_width = self.subviews.name_panel.frame_body.width + local wrapped_name = self.selected_note.point.name:wrap(note_width) + local wrapped_comment = self.selected_note.point.comment:wrap(note_width) + + self.subviews.name:setText(wrapped_name) + self.subviews.comment:setText(wrapped_comment) + self.subviews.note_details:updateLayout() +end + +function NotesWindow:reloadFilteredNotes() + self:loadFilteredNotes(self.curr_search_phrase, true) end function NotesWindow:loadFilteredNotes(search_phrase, force) @@ -234,24 +268,100 @@ function NotesWindow:loadFilteredNotes(search_phrase, force) local sel_ind, sel_note = self.subviews.note_list:getSelected() self:loadNote(sel_note) + self:updateLayout() end) end - NotesScreen = defclass(NotesScreen, gui.ZScreen) NotesScreen.ATTRS { focus_path='gui/notes', + pass_movement_keys=true, } function NotesScreen:init() + self.is_adding_note = false + self.adding_note_pos = nil self:addviews{ NotesWindow{ view_id='notes_window', frame={w=RESIZE_MIN.w, h=35}, + on_note_add=self:callback('startNoteAdd') }, } end +function NotesScreen:startNoteAdd() + self.adding_note_pos = nil + self.subviews.notes_window.visible = false + self.is_adding_note = true +end + +function NotesScreen:stopNoteAdd() + self.subviews.notes_window.visible = true + self.is_adding_note = false +end + +function NotesScreen:onInput(keys) + if self.is_adding_note then + if (keys.SELECT or keys._MOUSE_L) then + self.adding_note_pos = dfhack.gui.getMousePos() + + local manager = note_manager.NoteManager{ + note=nil, + on_update=function() + self.subviews.notes_window:reloadFilteredNotes() + dfhack.internal.runCommand('overlay trigger notes.map_notes') + self:dismiss() + end, + on_dismiss=function() + self:stopNoteAdd() + end + }:show() + manager:setNotePos(self.adding_note_pos) + + return true + elseif (keys.LEAVESCREEN or keys._MOUSE_R)then + self:stopNoteAdd() + return true + end + end + + return NotesScreen.super.onInput(self, keys) +end + +function NotesScreen:onRenderFrame(dc, rect) + NotesScreen.super.onRenderFrame(self, dc, rect) + + if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then + return + end + + if self.is_adding_note then + local curr_pos = self.adding_note_pos or dfhack.gui.getMousePos() + if not curr_pos then + return + end + + local function get_overlay_pen(pos) + if same_xy(curr_pos, pos) then + local texpos = dfhack.textures.getTexposByHandle(green_pin[1]) + return dfhack.pen.parse{ + ch='X', + fg=COLOR_BLUE, + tile=texpos + } + end + end + + guidm.renderMapOverlay(get_overlay_pen, { + x1=curr_pos.x, + y1=curr_pos.y, + x2=curr_pos.x, + y2=curr_pos.y, + }) + end +end + function NotesScreen:onDismiss() view = nil end diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 0b73494118..b5da3950b6 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -235,14 +235,18 @@ function TextEditor:renderSubviews(dc) end function TextEditor:onInput(keys) - if (self.subviews.scrollbar.is_dragging) then - return self.subviews.scrollbar:onInput(keys) - end - if keys._MOUSE_L and self:getMousePos() then self:setFocus(true) end + if not self.focus then + return false + end + + if (self.subviews.scrollbar.is_dragging) then + return self.subviews.scrollbar:onInput(keys) + end + return TextEditor.super.onInput(self, keys) end diff --git a/internal/notes/note_manager.lua b/internal/notes/note_manager.lua index 79c76b4e7f..fa0d35807a 100644 --- a/internal/notes/note_manager.lua +++ b/internal/notes/note_manager.lua @@ -16,6 +16,7 @@ NoteManager.ATTRS{ } function NoteManager:init() + self.note_pos = nil local edit_mode = self.note ~= nil self:addviews{ @@ -95,8 +96,12 @@ function NoteManager:init() } end +function NoteManager:setNotePos(note_pos) + self.notes_pos = note_pos +end + function NoteManager:createNote() - local cursor_pos = guidm.getCursorPos() + local cursor_pos = self.notes_pos or guidm.getCursorPos() if cursor_pos == nil then dfhack.printerr('Enable keyboard cursor to add a note.') return @@ -145,6 +150,9 @@ function NoteManager:saveNote() self.note.point.name = name self.note.point.comment = comment + if self.notes_pos then + self.note.pos=self.notes_pos + end if self.on_update then self.on_update() diff --git a/notes.lua b/notes.lua index deef9bfad3..f5a17788d4 100644 --- a/notes.lua +++ b/notes.lua @@ -36,7 +36,7 @@ function NotesOverlay:overlay_onupdate() end function NotesOverlay:overlay_trigger(args) - return self:showNoteManager() + self:reloadVisibleNotes() end function NotesOverlay:onInput(keys) From 936eed6d657a2f775c4189a612b319a272d66418 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Mon, 9 Sep 2024 22:18:44 +0200 Subject: [PATCH 089/811] Improve gui/notes note list navigation --- gui/notes.lua | 38 +++++++++++++++++--------------------- notes.lua | 4 ++-- 2 files changed, 19 insertions(+), 23 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 7022b71067..3df059dd2b 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -21,6 +21,18 @@ local green_pin = dfhack.textures.loadTileset( true ) +NotesSearchField = defclass(NotesSearchField, text_editor.TextEditor) +NotesSearchField.ATTRS {} + +function NotesSearchField:onInput(keys) + -- allow cursor up/down to be used to navigate the notes list + if keys.KEYBOARD_CURSOR_UP or keys.KEYBOARD_CURSOR_DOWN then + return false + end + + return NotesSearchField.super.onInput(self, keys) +end + NotesWindow = defclass(NotesWindow, widgets.Window) NotesWindow.ATTRS { frame_title='DF Notes', @@ -49,7 +61,7 @@ function NotesWindow:init() auto_width=true, on_activate=function() self.subviews.search:setFocus(true) end, }, - text_editor.TextEditor{ + NotesSearchField{ view_id='search', frame={l=0,h=3}, frame_style=gui.FRAME_INTERIOR, @@ -59,25 +71,10 @@ function NotesWindow:init() self.subviews.note_list:submit() end }, - widgets.Panel{ - frame={h=2}, - frame_inset={t=1}, - subviews={ - widgets.HotkeyLabel { - key='CUSTOM_ALT_L', - label='Notes', - frame={l=0,t=0}, - auto_width=true, - on_activate=function() - self.subviews.note_list:setFocus(true) - end, - }, - } - }, widgets.List{ view_id='note_list', frame={l=0,b=2}, - frame_inset={t=0}, + frame_inset={t=1}, row_height=1, on_submit=function (ind, note) self:loadNote(note) @@ -177,7 +174,7 @@ function NotesWindow:showNoteManager(note) note=note, on_update=function() self:reloadFilteredNotes() - dfhack.internal.runCommand('overlay trigger notes.map_notes') + dfhack.run_command_silent('overlay trigger notes.map_notes') end, on_dismiss=function() self.visible = true end } @@ -204,7 +201,7 @@ function NotesWindow:loadNote(note) return end - -- self.note_width_calculated = false + self:updateLayout() end function NotesWindow:postUpdateLayout() @@ -268,7 +265,6 @@ function NotesWindow:loadFilteredNotes(search_phrase, force) local sel_ind, sel_note = self.subviews.note_list:getSelected() self:loadNote(sel_note) - self:updateLayout() end) end @@ -310,7 +306,7 @@ function NotesScreen:onInput(keys) note=nil, on_update=function() self.subviews.notes_window:reloadFilteredNotes() - dfhack.internal.runCommand('overlay trigger notes.map_notes') + dfhack.run_command_silent('overlay trigger notes.map_notes') self:dismiss() end, on_dismiss=function() diff --git a/notes.lua b/notes.lua index f5a17788d4..1d192f34f8 100644 --- a/notes.lua +++ b/notes.lua @@ -35,7 +35,7 @@ function NotesOverlay:overlay_onupdate() self:reloadVisibleNotes() end -function NotesOverlay:overlay_trigger(args) +function NotesOverlay:overlay_trigger(cmd, title) self:reloadVisibleNotes() end @@ -164,7 +164,7 @@ local function main(args) return end - return dfhack.internal.runCommand('overlay trigger notes.map_notes') + return dfhack.run_command('overlay trigger notes.map_notes add') end end From 3100192e3f82f5c6492cb96c8690ca770b1119af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 10 Sep 2024 19:38:30 +0200 Subject: [PATCH 090/811] Simplify gui/notes note text preview wrapping feature --- gui/notes.lua | 28 ++++++++-------------------- 1 file changed, 8 insertions(+), 20 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 3df059dd2b..5824e7052f 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -107,7 +107,6 @@ function NotesWindow:init() view_id='note_details', frame={l=NOTE_LIST_RESIZE_MIN.w + 1,t=0,b=0}, frame_inset=1, - autoarrange_gap=1, subviews={ widgets.Panel{ view_id="name_panel", @@ -115,11 +114,11 @@ function NotesWindow:init() frame_style=gui.FRAME_INTERIOR, frame={l=0,r=0,t=0,h=4}, frame_inset={l=1,r=1}, - auto_height=true, subviews={ - widgets.Label{ + widgets.WrappedLabel{ view_id='name', - frame={t=0,l=0,r=0} + auto_height=false, + frame={l=0,r=0,t=0,b=0}, }, }, }, @@ -130,9 +129,10 @@ function NotesWindow:init() frame={l=0,r=0,t=4,b=2}, frame_inset={l=1,r=1,t=1}, subviews={ - widgets.Label{ + widgets.WrappedLabel{ view_id='comment', - frame={t=0,l=0,r=0} + auto_height=false, + frame={l=0,r=0,t=0,b=0}, }, } }, @@ -201,21 +201,9 @@ function NotesWindow:loadNote(note) return end - self:updateLayout() -end - -function NotesWindow:postUpdateLayout() - if self.selected_note == nil then - return - end - local note_details_frame = self.subviews.name_panel.frame_body - - local note_width = self.subviews.name_panel.frame_body.width - local wrapped_name = self.selected_note.point.name:wrap(note_width) - local wrapped_comment = self.selected_note.point.comment:wrap(note_width) + self.subviews.name.text_to_wrap = self.selected_note.point.name + self.subviews.comment.text_to_wrap = self.selected_note.point.comment - self.subviews.name:setText(wrapped_name) - self.subviews.comment:setText(wrapped_comment) self.subviews.note_details:updateLayout() end From f3b128d1d5ae5ad55c3d8fc13a51181d5468445c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 10 Sep 2024 19:44:25 +0200 Subject: [PATCH 091/811] Fix gui/notes issue with initial loading of selected note --- gui/notes.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gui/notes.lua b/gui/notes.lua index 5824e7052f..f04cf5cbe3 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -45,6 +45,7 @@ NotesWindow.ATTRS { function NotesWindow:init() self.selected_note = nil self.note_manager = nil + self.curr_search_phrase = nil self:addviews{ widgets.Panel{ @@ -161,8 +162,12 @@ function NotesWindow:init() } } } +end - self:loadFilteredNotes('', true) +function NotesWindow:postUpdateLayout() + if self.curr_search_phrase == nil then + self:loadFilteredNotes('', true) + end end function NotesWindow:showNoteManager(note) From 98220c06ffec1ae23d9f930f7a306e40a8a69cd0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 10 Sep 2024 19:51:28 +0200 Subject: [PATCH 092/811] Restore way to add notes by `notes add` command --- internal/notes/note_manager.lua | 1 + notes.lua | 10 +++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/internal/notes/note_manager.lua b/internal/notes/note_manager.lua index fa0d35807a..8d33bcc16c 100644 --- a/internal/notes/note_manager.lua +++ b/internal/notes/note_manager.lua @@ -2,6 +2,7 @@ local gui = require('gui') local widgets = require('gui.widgets') +local guidm = require('gui.dwarfmode') local text_editor = reqscript('internal/journal/text_editor') local waypoints = df.global.plotinfo.waypoints diff --git a/notes.lua b/notes.lua index 1d192f34f8..c4c9e78b3d 100644 --- a/notes.lua +++ b/notes.lua @@ -35,8 +35,12 @@ function NotesOverlay:overlay_onupdate() self:reloadVisibleNotes() end -function NotesOverlay:overlay_trigger(cmd, title) - self:reloadVisibleNotes() +function NotesOverlay:overlay_trigger(cmd) + if cmd == 'add' then + self:showNoteManager() + else + self:reloadVisibleNotes() + end end function NotesOverlay:onInput(keys) @@ -164,7 +168,7 @@ local function main(args) return end - return dfhack.run_command('overlay trigger notes.map_notes add') + return dfhack.run_command_silent('overlay trigger notes.map_notes add') end end From 01fe42c1cac175cddd642522ba464b59b7420913 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 10 Sep 2024 20:03:39 +0200 Subject: [PATCH 093/811] Add basic documentation for `gui/notes` tool --- docs/gui/notes.rst | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 docs/gui/notes.rst diff --git a/docs/gui/notes.rst b/docs/gui/notes.rst new file mode 100644 index 0000000000..d10b5b6d57 --- /dev/null +++ b/docs/gui/notes.rst @@ -0,0 +1,33 @@ +gui/notes +========= + +.. dfhack-tool:: + :summary: Interactive panel for managing map-specific notes. + :tags: fort interface map + +The `gui/notes` tool provides a comprehensive interface for interacting +with map-specific notes. It is designed to streamline the process +of note management in Dwarf Fortress, making it simpler +and more intuitive to keep track of important map-specific information. + +This tool builds upon the functionality of the `notes` tool, +enhancing it by providing a user-friendly panel for easier management +and visibility of notes across the Dwarf Fortress game map. + +Usage +----- + +:: + + gui/notes + +Launch the notes management panel. + +Supported Features +------------------ + +- Interactive Panel: Manage all aspects of notes through a centralized graphical interface. +- Search (:kbd:`Alt` + :kbd:`S`): Quickly find notes +- Direct Map Interaction (:kbd:`Alt` + :kbd:`N`): Add new notes by clicking on the map. +- Edit (:kbd:`Alt` + :kbd:`U`): Easily modify existing notes. +- Delete (:kbd:`Alt` + :kbd:`D`): Easily remove existing notes. From 5ff4e50b70f189f79aac2fab214a2dd06e6d72e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 10 Sep 2024 20:40:23 +0200 Subject: [PATCH 094/811] Auto enable notes overlay when `gui/notes` is visible --- gui/notes.lua | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/gui/notes.lua b/gui/notes.lua index f04cf5cbe3..d6354ac1e3 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -5,6 +5,7 @@ local gui = require 'gui' local widgets = require 'gui.widgets' local guidm = require('gui.dwarfmode') local script = require 'gui.script' +local overlay = require 'plugins.overlay' local text_editor = reqscript('internal/journal/text_editor') local note_manager = reqscript('internal/notes/note_manager') @@ -13,6 +14,7 @@ local map_points = df.global.plotinfo.waypoints.points local NOTE_LIST_RESIZE_MIN = {w=26} local RESIZE_MIN = {w=65, h=30} local NOTE_SEARCH_BATCH_SIZE = 25 +local OVERLAY_NAME = 'notes.map_notes' local green_pin = dfhack.textures.loadTileset( 'hack/data/art/note_green_pin_map.png', @@ -351,7 +353,17 @@ function NotesScreen:onRenderFrame(dc, rect) end end +function NotesScreen:onAboutToShow() + if not overlay.get_state().config[OVERLAY_NAME].enabled then + self.should_disable_overlay = true + overlay.overlay_command({'enable', 'notes.map_notes'}) + end +end + function NotesScreen:onDismiss() + if self.should_disable_overlay then + overlay.overlay_command({'disable', 'notes.map_notes'}) + end view = nil end From d8c6cac2676af5a690baedc8669f7b8a04c4855c Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Wed, 11 Sep 2024 15:52:19 +0200 Subject: [PATCH 095/811] idle-crafting: make choice of crafting job dependent on resources in linked stockpiles. --- changelog.txt | 1 + docs/idle-crafting.rst | 15 +++- idle-crafting.lua | 182 +++++++++++++++++++++++++++++++++++++---- 3 files changed, 180 insertions(+), 18 deletions(-) diff --git a/changelog.txt b/changelog.txt index d4b6641e58..50bfd543b0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: - `position`: option to copy keyboard cursor position to the clipboard - `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) - `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use +- `idle-crafting`: make choice of crafting job dependent on resources in linked stockpiles. ## Documentation - `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses diff --git a/docs/idle-crafting.rst b/docs/idle-crafting.rst index 39086443a9..d1cd23c512 100644 --- a/docs/idle-crafting.rst +++ b/docs/idle-crafting.rst @@ -45,9 +45,16 @@ needs to craft objects. Workshops that have a master assigned cannot be used in this way. When a workshop is designated for idle crafting, this tool will create crafting -jobs and assign them to idle dwarves who have a need for crafting -objects. Currently, bone carving and stonecrafting are supported, with -stonecrafting being the default option. This script respects the setting for -permitted general work orders from the "Workers" tab. Thus, to designate a +jobs and assign them to idle dwarves who have a need for crafting objects. This +script respects the setting for permitted general work orders from the "Workers" +tab. + +For workshops without input stockpile links, bone carving and stonecrafting are +supported, with stonecrafting being the default option. Thus, to designate a workshop for bone carving, disable the stonecrafting labor while keeping the bone carving labor enabled. + +For workshops with input stockpile links, the creation of totems and horn crafts +are supported as well. In this case, the choice of job is made randomly based on +the resources available in the input stockpiles (respecting the permitted +labors from the workshop profile). diff --git a/idle-crafting.lua b/idle-crafting.lua index b618a0a67f..92673f51b8 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -6,6 +6,55 @@ local widgets = require('gui.widgets') local repeatutil = require("repeat-util") local orders = require('plugins.orders') +---iterate over input materials of workshop with stockpile links +---@param workshop df.building_workshopst +---@param action fun(item:df.item):any +local function for_inputs(workshop, action) + if #workshop.profile.links.take_from_pile == 0 then + dfhack.error('workshop has no links') + else + for _, stockpile in ipairs(workshop.profile.links.take_from_pile) do + for _, item in ipairs(dfhack.buildings.getStockpileContents(stockpile)) do + if(item:isAssignedToThisStockpile(stockpile.id)) then + for _, contained_item in ipairs(dfhack.items.getContainedItems(item)) do + action(contained_item) + end + else + action(item) + end + end + end + for _, contained_item in ipairs(workshop.contained_items) do + if contained_item.use_mode == 0 then + action(contained_item.item) + end + end + end +end + +---choose random value based on positive integer weights +---@generic T +---@param choices table +---@return T +function weightedChoice(choices) + local sum = 0 + for _, weight in pairs(choices) do + sum = sum + weight + end + if sum <= 0 then + return nil + end + local random = math.random(sum) + for choice, weight in pairs(choices) do + if random > weight then + random = random - weight + else + return choice + end + end + return nil --never reached on well-formed input +end + ---create a new linked job ---@return df.job function make_job() @@ -14,6 +63,61 @@ function make_job() return job end +function assignToWorkshop(job, workshop) + job.pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z) + dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, workshop.id) + workshop.jobs:insert("#", job) +end + +---make totem at specified workshop +---@param unit df.unit +---@param workshop df.building_workshopst +---@return boolean +function makeTotem(unit, workshop) + local job = make_job() + job.job_type = df.job_type.MakeTotem + job.mat_type = -1 + + local jitem = df.job_item:new() + jitem.item_type = df.item_type.NONE --the game seems to leave this uninitialized + jitem.mat_type = -1 + jitem.mat_index = -1 + jitem.quantity = 1 + jitem.vector_id = df.job_item_vector_id.ANY_REFUSE + jitem.flags1.unrotten = true + jitem.flags2.totemable = true + jitem.flags2.body_part = true + job.job_items.elements:insert('#', jitem) + + assignToWorkshop(job, workshop) + return dfhack.job.addWorker(job, unit) +end + +---make totem at specified workshop +---@param unit df.unit +---@param workshop df.building_workshopst +---@return boolean +function makeHornCrafts(unit, workshop) + local job = make_job() + job.job_type = df.job_type.MakeCrafts + job.mat_type = -1 + job.material_category.horn = true + + local jitem = df.job_item:new() + jitem.item_type = df.item_type.NONE --the game seems to leave this uninitialized + jitem.mat_type = -1 + jitem.mat_index = -1 + jitem.quantity = 1 + jitem.vector_id = df.job_item_vector_id.ANY_REFUSE + jitem.flags1.unrotten = true + jitem.flags2.horn = true + jitem.flags2.body_part = true + job.job_items.elements:insert('#', jitem) + + assignToWorkshop(job, workshop) + return dfhack.job.addWorker(job, unit) +end + ---make bone crafts at specified workshop ---@param unit df.unit ---@param workshop df.building_workshopst @@ -23,7 +127,6 @@ function makeBoneCraft(unit, workshop) job.job_type = df.job_type.MakeCrafts job.mat_type = -1 job.material_category.bone = true - job.pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z) local jitem = df.job_item:new() jitem.item_type = df.item_type.NONE @@ -36,8 +139,7 @@ function makeBoneCraft(unit, workshop) jitem.flags2.body_part = true job.job_items.elements:insert('#', jitem) - dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, workshop.id) - workshop.jobs:insert("#", job) + assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end @@ -49,7 +151,6 @@ function makeRockCraft(unit, workshop) local job = make_job() job.job_type = df.job_type.MakeCrafts job.mat_type = 0 - job.pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z) local jitem = df.job_item:new() jitem.item_type = df.item_type.BOULDER @@ -61,12 +162,27 @@ function makeRockCraft(unit, workshop) jitem.flags3.hard = true job.job_items.elements:insert('#', jitem) - dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, workshop.id) - workshop.jobs:insert("#", job) - + assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end +---categorize and count crafting materials (for Craftsdwarf's workshop) +---@param tab table +---@param item df.item +local function categorize_craft(tab,item) + if df.item_corpsepiecest:is_instance(item) then + if item.corpse_flags.bone then + tab['bone'] = (tab['bone'] or 0) + item.material_amount.Bone + elseif item.corpse_flags.skull then + tab['skull'] = (tab['skull'] or 0) + 1 + elseif item.corpse_flags.horn then + tab['horn'] = (tab['horn'] or 0) + item.material_amount.Horn + end + elseif df.item_boulderst:is_instance(item) then + tab['boulder'] = (tab['boulder'] or 0) + 1 + end +end + -- script logic local GLOBAL_KEY = 'idle-crafting' @@ -180,6 +296,32 @@ function unitIsAvailable(unit) return true end +---select crafting job based on available resources +---@param workshop df.building_workshopst +---@return (fun(unit:df.unit, workshop:df.building_workshopst):boolean)? +function select_crafting_job(workshop) + local tab = {} + for_inputs(workshop, curry(categorize_craft,tab)) + local blocked_labors = workshop.profile.blocked_labors + if blocked_labors[STONE_CRAFT] then + tab['boulder'] = nil + end + if blocked_labors[BONE_CARVE] then + tab['bone'] = nil + tab['skull'] = nil + tab['horn'] = nil + end + local material = weightedChoice(tab) + if material == 'bone' then return makeBoneCraft + elseif material == 'skull' then return makeTotem + elseif material == 'horn' then return makeHornCrafts + elseif material == 'boulder' then return makeRockCraft + else + return nil + end +end + + ---check if unit is ready and try to create a crafting job for it ---@param workshop df.building_workshopst ---@param idx integer "index of the unit's group" @@ -200,19 +342,31 @@ local function processUnit(workshop, idx, unit_id) end -- We have an available unit local success = false - if workshop.profile.blocked_labors[STONE_CRAFT] == false then - success = makeRockCraft(unit, workshop) - end - if not success and workshop.profile.blocked_labors[BONE_CARVE] == false then - success = makeBoneCraft(unit, workshop) + if #workshop.profile.links.take_from_pile == 0 then + -- can we do something smarter here? + if workshop.profile.blocked_labors[STONE_CRAFT] == false then + success = makeRockCraft(unit, workshop) + end + if not success and workshop.profile.blocked_labors[BONE_CARVE] == false then + success = makeBoneCraft(unit, workshop) + end + if not success then + dfhack.printerr('idle-crafting: profile allows neither bone carving nor stonecrafting') + end + else + local craftItem = select_crafting_job(workshop) + if craftItem then + success = craftItem(unit, workshop) + else + print('idle-crafting: workshop has no usable materials in linked stockpiles') + failing[workshop.id] = true + end end if success then -- Why is the encoding still wrong, even when using df2console? print('idle-crafting: assigned crafting job to ' .. dfhack.df2console(dfhack.units.getReadableName(unit))) watched[idx][unit_id] = nil allowed[workshop.id] = df.global.world.frame_counter - else - dfhack.printerr('idle-crafting: profile allows neither bone carving nor stonecrafting, disabling workshop') end return true end From a342a227374d16414a70ef1ca98332abf6b44ff3 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 12 Sep 2024 18:41:47 +0200 Subject: [PATCH 096/811] new tool: immortal-cravings --- changelog.txt | 1 + docs/immortal-cravings.rst | 18 +++ immortal-cravings.lua | 231 +++++++++++++++++++++++++++++++++++++ 3 files changed, 250 insertions(+) create mode 100644 docs/immortal-cravings.rst create mode 100644 immortal-cravings.lua diff --git a/changelog.txt b/changelog.txt index d4b6641e58..6ad27e1a5b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: - `idle-crafting`: allow dwarves to independently satisfy their need to craft objects - `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationships - `notes`: manage map-specific notes +- `immortal-cravings`: allow immortals to satisfy their cravings for food and drink ## New Features - `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them diff --git a/docs/immortal-cravings.rst b/docs/immortal-cravings.rst new file mode 100644 index 0000000000..2fb851b9d6 --- /dev/null +++ b/docs/immortal-cravings.rst @@ -0,0 +1,18 @@ +immortal-cravings +================= + +.. dfhack-tool:: + :summary: Allow immortals to satisfy their cravings for food and drink. + :tags: fort gameplay + +When enabled, this script watches your fort for units that have no physiological +need to eat or drink but still have personality needs that can only be satisfied +by eating or drinking (e.g. necromancers). This enables those units to help +themselves to a drink or a meal when they crave one and are not otherwise +occupied. + +Usage +----- + +``enable immortal-cravings`` +``disable immortal-cravings`` diff --git a/immortal-cravings.lua b/immortal-cravings.lua new file mode 100644 index 0000000000..c945b44f8f --- /dev/null +++ b/immortal-cravings.lua @@ -0,0 +1,231 @@ +--@enable = true +--@module = true + +local idle = reqscript('idle-crafting') +local repeatutil = require("repeat-util") +--- utility functions + +---3D city metric +---@param p1 df.coord +---@param p2 df.coord +---@return number +function distance(p1, p2) + return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) + math.abs(p1.z - p2.z) +end + +---find closest accessible item in an item vector +---@generic T : df.item +---@param pos df.coord +---@param item_vector T[] +---@param is_good? fun(item: T): boolean +---@return T? +local function findClosest(pos, item_vector, is_good) + local closest = nil + local dclosest = -1 + for _,item in ipairs(item_vector) do + if not item.flags.in_job and (not is_good or is_good(item)) then + local x, y, z = dfhack.items.getPosition(item) + local pitem = xyz2pos(x, y, z) + local ditem = distance(pos, pitem) + if dfhack.maps.canWalkBetween(pos, pitem) and (not closest or ditem < dclosest) then + closest = item + dclosest = ditem + end + end + end + return closest +end + +---find a drink +---@param pos df.coord +---@return df.item_drinkst|nil +local function get_closest_drink(pos) + local is_good = function (drink) + local container = dfhack.items.getContainer(drink) + return container and df.item_barrelst:is_instance(container) + end + return findClosest(pos, df.global.world.items.other.DRINK, is_good) +end + +---find some prepared meal +---@return df.item_foodst? +local function get_closest_meal(pos) + ---@param meal df.item_foodst + local function is_good(meal) + return meal.flags.rotten == false + end + return findClosest(pos, df.global.world.items.other.FOOD, is_good) +end + +---create a Drink job for the given unit +---@param unit df.unit +local function goDrink(unit) + local drink = get_closest_drink(unit.pos) + if not drink then + -- print('no accessible drink found') + return + end + local job = idle.make_job() + job.job_type = df.job_type.DrinkItem + job.flags.special = true + local dx, dy, dz = dfhack.items.getPosition(drink) + job.pos = xyz2pos(dx, dy, dz) + if not dfhack.job.attachJobItem(job, drink, df.job_item_ref.T_role.Other, -1, -1) then + error('could not attach drink') + return + end + dfhack.job.addWorker(job, unit) + local name = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) + print(dfhack.df2console('immortal-cravings: %s is getting a drink'):format(name)) +end + +---create Eat job for the given unit +---@param unit df.unit +local function goEat(unit) + local meal = get_closest_meal(unit.pos) + if not meal then + -- print('no accessible meals found') + return + end + local job = idle.make_job() + job.job_type = df.job_type.Eat + job.flags.special = true + local dx, dy, dz = dfhack.items.getPosition(meal) + job.pos = xyz2pos(dx, dy, dz) + if not dfhack.job.attachJobItem(job, meal, df.job_item_ref.T_role.Other, -1, -1) then + error('could not attach meal') + return + end + dfhack.job.addWorker(job, unit) + local name = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) + print(dfhack.df2console('immortal-cravings: %s is getting something to eat'):format(name)) +end + +--- script logic + +local GLOBAL_KEY = 'immortal-cravings' + +enabled = enabled or false +function isEnabled() + return enabled +end + +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=enabled, + }) +end + +--- Load the saved state of the script +local function load_state() + -- load persistent data + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + enabled = persisted_data.enabled or false +end + +DrinkAlcohol = df.need_type['DrinkAlcohol'] +EatGoodMeal = df.need_type['EatGoodMeal'] + +---@type integer[] +watched = {} + +threshold = -9000 + +---unit loop: check for idle watched units and create eat/drink jobs for them +local function unit_loop() + -- print(('immortal-cravings: running unit loop (%d watched units)'):format(#watched)) + ---@type integer[] + local kept = {} + for _, unit_id in ipairs(watched) do + local unit = df.unit.find(unit_id) + if unit and not (unit.flags1.caged or unit.flags1.chained) then + if not idle.unitIsAvailable(unit) then + table.insert(kept, unit.id) + else + -- + for _, need in ipairs(unit.status.current_soul.personality.needs) do + if need.id == DrinkAlcohol and need.focus_level < threshold then + goDrink(unit) + goto next_unit + elseif need.id == EatGoodMeal and need.focus_level < threshold then + goEat(unit) + goto next_unit + end + end + end + else + -- print('immortal-cravings: unit gone or caged') + end + ::next_unit:: + end + watched = kept + if #watched == 0 then + -- print('immortal-cravings: no more watched units, cancelling unit loop') + repeatutil.cancel(GLOBAL_KEY .. '-unit') + end +end + +---main loop: look for citizens with personality needs for food/drink but w/o physiological need +local function main_loop() + print('immortal-cravings watching:') + watched = {} + for _, unit in ipairs(dfhack.units.getCitizens()) do + if unit.curse.add_tags1.NO_DRINK or unit.curse.add_tags1.NO_EAT then + for _, need in ipairs(unit.status.current_soul.personality.needs) do + if need.id == DrinkAlcohol and need.focus_level < threshold or + need.id == EatGoodMeal and need.focus_level < threshold + then + table.insert(watched, unit.id) + print(' '..dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit)))) + goto next_unit + end + end + end + ::next_unit:: + end + + if #watched > 0 then + repeatutil.scheduleUnlessAlreadyScheduled(GLOBAL_KEY..'-unit', 59, 'ticks', unit_loop) + end +end + +local function start() + if enabled then + repeatutil.scheduleUnlessAlreadyScheduled(GLOBAL_KEY..'-main', 4003, 'ticks', main_loop) + end +end + +local function stop() + repeatutil.cancel(GLOBAL_KEY..'-main') + repeatutil.cancel(GLOBAL_KEY..'-unit') +end + + + +-- script action + +--- Handles automatic loading +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + enabled = false + return + end + + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + load_state() + start() +end + +if dfhack_flags.enable then + if dfhack_flags.enable_state then + enabled = true + start() + else + enabled = false + stop() + end + persist_state() +end From 4795ff5ee707f74c7fe95db1d557f5be6c5e8b3e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Sep 2024 14:30:44 -0700 Subject: [PATCH 097/811] show descriptive names for races still takes IDs on the commandline, though --- changelog.txt | 1 + exterminate.lua | 18 +++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index d4b6641e58..ae1b4851e7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -57,6 +57,7 @@ Template for new versions: - `position`: option to copy keyboard cursor position to the clipboard - `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) - `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use +- `exterminate`: show descriptive names for the listed races in addition to their IDs ## Documentation - `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses diff --git a/exterminate.lua b/exterminate.lua index 06b764eb98..cb893afe78 100644 --- a/exterminate.lua +++ b/exterminate.lua @@ -134,10 +134,12 @@ local function getMapRaces(opts) local map_races = {} for _, unit in pairs(df.global.world.units.active) do if not checkUnit(opts, unit) then goto continue end - local unit_race_name = dfhack.units.isUndead(unit) and "UNDEAD" or df.creature_raw.find(unit.race).creature_id + local craw = df.creature_raw.find(unit.race) + local unit_race_name = dfhack.units.isUndead(unit) and 'UNDEAD' or craw.creature_id local race = ensure_key(map_races, unit_race_name) race.id = unit.race race.name = unit_race_name + race.display_name = unit_race_name == 'UNDEAD' and '' or craw.name[0] race.count = (race.count or 0) + 1 ::continue:: end @@ -187,14 +189,20 @@ local map_races = getMapRaces(options) if not positionals[1] or positionals[1] == 'list' then local sorted_races = {} - for race, value in pairs(map_races) do - table.insert(sorted_races, { name = race, count = value.count }) + local max_width = 10 + for _,v in pairs(map_races) do + max_width = math.max(max_width, #v.name) + table.insert(sorted_races, v) end table.sort(sorted_races, function(a, b) return a.count > b.count end) - for _, race in ipairs(sorted_races) do - print(([[%4s %s]]):format(race.count, race.name)) + for _,v in ipairs(sorted_races) do + local name_str = v.name + if name_str ~= 'UNDEAD' then + name_str = ('%-'..tostring(max_width)..'s (%s)'):format(name_str, v.display_name) + end + print(('%4s %s'):format(v.count, name_str)) end return end From 5a67b828be37b3d399d1a4d3fc88afbb374d2e72 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Sep 2024 14:37:41 -0700 Subject: [PATCH 098/811] don't display descriptive name if same as id --- exterminate.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exterminate.lua b/exterminate.lua index cb893afe78..0043febb94 100644 --- a/exterminate.lua +++ b/exterminate.lua @@ -199,7 +199,7 @@ if not positionals[1] or positionals[1] == 'list' then end) for _,v in ipairs(sorted_races) do local name_str = v.name - if name_str ~= 'UNDEAD' then + if name_str ~= 'UNDEAD' and v.display_name ~= string.lower(name_str):gsub('_', ' ') then name_str = ('%-'..tostring(max_width)..'s (%s)'):format(name_str, v.display_name) end print(('%4s %s'):format(v.count, name_str)) From c16b4664472e6317f7a192d5af110e9b46f5d194 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Sep 2024 15:33:45 -0700 Subject: [PATCH 099/811] add help button for idle-crafting overlay --- idle-crafting.lua | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/idle-crafting.lua b/idle-crafting.lua index b618a0a67f..aefd44b722 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -356,18 +356,20 @@ end IdleCraftingOverlay = defclass(IdleCraftingOverlay, overlay.OverlayWidget) IdleCraftingOverlay.ATTRS { desc = "Adds a toggle for recreational crafting to Craftdwarf's workshops.", - default_pos = { x = -42, y = 41 }, + default_pos = { x = -39, y = 41 }, + version = 2, default_enabled = true, viewscreens = { 'dwarfmode/ViewSheets/BUILDING/Workshop/Craftsdwarfs/Workers', }, - frame = { w = 54, h = 1 }, + frame = { w = 58, h = 1 }, visible = orders.can_set_labors } function IdleCraftingOverlay:init() self:addviews { widgets.BannerPanel{ + frame={l=0, w=54}, subviews={ widgets.CycleHotkeyLabel { view_id = 'leisure_toggle', @@ -388,6 +390,10 @@ function IdleCraftingOverlay:init() } }, }, + widgets.HelpButton{ + frame={r=0}, + command='idle-crafting', + }, } end From a14e732f0655633922ccf28c48a764257de7138d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Sep 2024 17:33:59 -0700 Subject: [PATCH 100/811] check and fix room ownership links --- changelog.txt | 1 + docs/fix/ownership.rst | 17 +++++++++------ fix/ownership.lua | 48 +++++++++++++++++++++++++++++++++--------- 3 files changed, 50 insertions(+), 16 deletions(-) diff --git a/changelog.txt b/changelog.txt index ae1b4851e7..7444ef3d31 100644 --- a/changelog.txt +++ b/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: - `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) - `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use - `exterminate`: show descriptive names for the listed races in addition to their IDs +- `fix/ownership`: now also checks and fixes room ownership links ## Documentation - `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses diff --git a/docs/fix/ownership.rst b/docs/fix/ownership.rst index 18ee5518a9..a0932ac5c6 100644 --- a/docs/fix/ownership.rst +++ b/docs/fix/ownership.rst @@ -2,15 +2,20 @@ fix/ownership ============= .. dfhack-tool:: - :summary: Fixes instances of units claiming the same item or an item they don't own. - :tags: fort bugfix units + :summary: Fixes ownership links. + :tags: fort bugfix items units -Due to a bug a unit can believe they own an item when they actually do not. +Due to a bug, a unit can believe they own an item when they actually do not. +Additionally, a room can remember that it is owned by a unit, but the unit can +forget that they own the room. -When enabled in `gui/control-panel`, `fix/ownership` will run once a day to check citizens and residents and make sure they don't -mistakenly own an item they shouldn't. +Invalid item ownership links result in units getting stuck in a "Store owned +item" job. Missing room ownership links result in rooms becoming unused by the +nominal owner and unclaimable by any other unit. In particular, nobles and +administrators will not recognize that their room requirements are met. -This should help issues of units getting stuck in a "Store owned item" job. +When enabled in `gui/control-panel`, `fix/ownership` will run once a day to +validate and fix ownership links for items and rooms. Usage ----- diff --git a/fix/ownership.lua b/fix/ownership.lua index b21b818a7e..c98dd42476 100644 --- a/fix/ownership.lua +++ b/fix/ownership.lua @@ -1,24 +1,51 @@ +local utils = require('utils') + -- unit thinks they own the item but the item doesn't hold the proper -- ref that actually makes this true -local function owner_not_recognized() +local function clean_item_ownership() for _,unit in ipairs(dfhack.units.getCitizens()) do for index = #unit.owned_items-1, 0, -1 do - local item = df.item.find(unit.owned_items[index]) - if not item then goto continue end - - for _, ref in ipairs(item.general_refs) do - if df.general_ref_unit_itemownerst:is_instance(ref) then - -- make sure the ref belongs to unit - if ref.unit_id == unit.id then goto continue end + local item_id = unit.owned_items[index] + local item = df.item.find(item_id) + if item then + for _, ref in ipairs(item.general_refs) do + if df.general_ref_unit_itemownerst:is_instance(ref) then + -- make sure the ref belongs to unit + if ref.unit_id == unit.id then goto continue end + end end end - print('Erasing ' .. dfhack.TranslateName(unit.name) .. ' invalid claim on item #' .. item.id) + print(('fix/ownership: Erasing invalid claim on item #%d for %s'):format( + item_id, dfhack.df2console(dfhack.units.getReadableName(unit)))) unit.owned_items:erase(index) ::continue:: end end end +local other = df.global.world.buildings.other +local zone_vecs = { + other.ZONE_BEDROOM, + other.ZONE_OFFICE, + other.ZONE_DINING_HALL, + other.ZONE_TOMB, +} +local function relink_zones() + for _,zones in ipairs(zone_vecs) do + for _,zone in ipairs(zones) do + local unit = zone.assigned_unit + if not unit then goto continue end + if not utils.linear_index(unit.owned_buildings, zone.id, 'id') then + print(('fix/ownership: Restoring %s ownership link for %s'):format( + df.civzone_type[zone:getSubtype()], dfhack.df2console(dfhack.units.getReadableName(unit)))) + dfhack.buildings.setOwner(zone, nil) + dfhack.buildings.setOwner(zone, unit) + end + ::continue:: + end + end +end + local args = {...} if args[1] == "help" then @@ -26,4 +53,5 @@ if args[1] == "help" then return end -owner_not_recognized() +clean_item_ownership() +relink_zones() From b1f21fa5a03c094651571aa7ee5993fee79d07b0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 13 Sep 2024 17:35:45 -0700 Subject: [PATCH 101/811] refer to DF bug that this fixes --- docs/fix/ownership.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/fix/ownership.rst b/docs/fix/ownership.rst index a0932ac5c6..44c412db84 100644 --- a/docs/fix/ownership.rst +++ b/docs/fix/ownership.rst @@ -23,3 +23,8 @@ Usage :: fix/ownership + +Links +----- + +Among other issues, this tool fixes :bug:`6578`. From 5c4a9b12b2f654ddd18534af0c2060dd6eb620f9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 14 Sep 2024 10:37:33 -0700 Subject: [PATCH 102/811] display actual names for unique beasts --- changelog.txt | 1 + exterminate.lua | 32 +++++++++++++++++++++++++++----- 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 7444ef3d31..cd5d2baba1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -58,6 +58,7 @@ Template for new versions: - `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) - `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use - `exterminate`: show descriptive names for the listed races in addition to their IDs +- `exterminate`: show actual names for unique creatures such as forgotten beasts and titans - `fix/ownership`: now also checks and fixes room ownership links ## Documentation diff --git a/exterminate.lua b/exterminate.lua index 0043febb94..0263fbe483 100644 --- a/exterminate.lua +++ b/exterminate.lua @@ -134,12 +134,23 @@ local function getMapRaces(opts) local map_races = {} for _, unit in pairs(df.global.world.units.active) do if not checkUnit(opts, unit) then goto continue end - local craw = df.creature_raw.find(unit.race) - local unit_race_name = dfhack.units.isUndead(unit) and 'UNDEAD' or craw.creature_id - local race = ensure_key(map_races, unit_race_name) + local race_name, display_name + if dfhack.units.isUndead(unit) then + race_name = 'UNDEAD' + display_name = 'UNDEAD' + else + local craw = df.creature_raw.find(unit.race) + race_name = craw.creature_id + if race_name:match('^FORGOTTEN_BEAST_[0-9]+$') or race_name:match('^TITAN_[0-9]+$') then + display_name = dfhack.units.getReadableName(unit) + else + display_name = craw.name[0] + end + end + local race = ensure_key(map_races, race_name) race.id = unit.race - race.name = unit_race_name - race.display_name = unit_race_name == 'UNDEAD' and '' or craw.name[0] + race.name = race_name + race.display_name = display_name race.count = (race.count or 0) + 1 ::continue:: end @@ -195,6 +206,17 @@ if not positionals[1] or positionals[1] == 'list' then table.insert(sorted_races, v) end table.sort(sorted_races, function(a, b) + if a.count == b.count then + local asuffix, bsuffix = a.name:match('([0-9]+)$'), b.name:match('([0-9]+)$') + if asuffix and bsuffix then + local aname, bname = a.name:match('(.*)_[0-9]+$'), b.name:match('(.*)_[0-9]+$') + local anum, bnum = tonumber(asuffix), tonumber(bsuffix) + if aname == bname and anum and bnum then + return anum < bnum + end + end + return a.name < b.name + end return a.count > b.count end) for _,v in ipairs(sorted_races) do From 0dec6333c39372622db01ab3b50de57a633656c6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 14 Sep 2024 13:28:19 -0700 Subject: [PATCH 103/811] migrate TextEditor to use new text navigation keys --- docs/gui/journal.rst | 27 ++++++++------ internal/journal/text_editor.lua | 11 +++--- test/gui/journal.lua | 64 ++++++++++++++++---------------- 3 files changed, 53 insertions(+), 49 deletions(-) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index d6004ec95d..675251e24b 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -8,23 +8,28 @@ gui/journal The `gui/journal` interface makes it easy to take notes and document important details for the fortresses. -With this multi-line text editor, -you can keep track of your fortress's background story, goals, notable events, -and both short-term and long-term plans. +With this multi-line text editor, you can keep track of your fortress's +background story, goals, notable events, and both short- and long-term plans. This is particularly useful when you need to take a longer break from the game. -Having detailed notes makes it much easier to resume your game after -a few weeks or months, without losing track of your progress and objectives. +Having detailed notes makes it much easier to resume your game after a few +weeks or months without losing track of your progress and objectives. Supported Features ------------------ -- Cursor Control: Navigate through text using arrow keys (left, right, up, down) for precise cursor placement. -- Fast Rewind: Use :kbd:`Ctrl` + :kbd:`Left` / :kbd:`Ctrl` + :kbd:`B` and :kbd:`Ctrl` + :kbd:`Right` / :kbd:`Ctrl` + :kbd:`F` to move the cursor one word back or forward. -- Longest X Position Memory: The cursor remembers the longest x position when moving up or down, making vertical navigation more intuitive. -- Mouse Control: Use the mouse to position the cursor within the text, providing an alternative to keyboard navigation. -- New Lines: Easily insert new lines using the :kbd:`Enter` key, supporting multiline text input. -- Text Wrapping: Text automatically wraps within the editor, ensuring lines fit within the display without manual adjustments. +- Cursor Control: Navigate through text using arrow keys (Left, Right, Up, + and Down) for precise cursor placement. +- Fast Rewind: Use :kbd:`Ctrl` + :kbd:`Left` and :kbd:`Ctrl` + :kbd:`Right` to + move the cursor one word back or forward. +- Longest X Position Memory: The cursor remembers the longest x position when + moving up or down, making vertical navigation more intuitive. +- Mouse Control: Use the mouse to position the cursor within the text, + providing an alternative to keyboard navigation. +- New Lines: Easily insert new lines using the :kbd:`Enter` key, supporting + multiline text input. +- Text Wrapping: Text automatically wraps within the editor, ensuring lines fit + within the display without manual adjustments. - Backspace Support: Use the backspace key to delete characters to the left of the cursor. - Delete Character: :kbd:`Ctrl` + :kbd:`D` deletes the character under the cursor. - Line Navigation: :kbd:`Ctrl` + :kbd:`H` (like "Home") moves the cursor to the beginning of the current line, and :kbd:`Ctrl` + :kbd:`E` (like "End") moves it to the end. diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index cf15776d46..030668714f 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -768,23 +768,23 @@ function TextEditorView:onCursorInput(keys) -- go to text end self:setCursor(#self.text + 1) return true - elseif keys.CUSTOM_CTRL_B or keys.A_MOVE_W_DOWN then + elseif keys.CUSTOM_CTRL_LEFT then -- back one word local word_start = self:wordStartOffset() self:setCursor(word_start) return true - elseif keys.CUSTOM_CTRL_F or keys.A_MOVE_E_DOWN then + elseif keys.CUSTOM_CTRL_RIGHT then -- forward one word local word_end = self:wordEndOffset() self:setCursor(word_end) return true - elseif keys.CUSTOM_CTRL_H then + elseif keys.CUSTOM_HOME then -- line start self:setCursor( self:lineStartOffset() ) return true - elseif keys.CUSTOM_CTRL_E then + elseif keys.CUSTOM_END then -- line end self:setCursor( self:lineEndOffset() @@ -878,8 +878,7 @@ function TextEditorView:onTextManipulationInput(keys) self:eraseSelection() return true - elseif keys.CUSTOM_CTRL_D then - -- delete char, there is no support for `Delete` key + elseif keys.CUSTOM_DELETE then self.history:store(HISTORY_ENTRY.DELETE, self.text, self.cursor) if (self:hasSelection()) then diff --git a/test/gui/journal.lua b/test/gui/journal.lua index d845aef20e..b73a37a9a2 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -772,7 +772,7 @@ function test.handle_delete() text_area:setCursor(1) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '_: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -783,7 +783,7 @@ function test.handle_delete() text_area:setCursor(124) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -794,7 +794,7 @@ function test.handle_delete() text_area:setCursor(123) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -805,7 +805,7 @@ function test.handle_delete() text_area:setCursor(171) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -815,7 +815,7 @@ function test.handle_delete() }, '\n')); for i=1,59 do - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') end expect.eq(read_rendered_text(text_area), table.concat({ @@ -824,7 +824,7 @@ function test.handle_delete() 'nibhorttitor mi, vitae rutrum eros metus nec libero._', }, '\n')); - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -849,7 +849,7 @@ function test.line_end() text_area:setCursor(1) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_E') + simulate_input_keys('CUSTOM_END') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', @@ -861,7 +861,7 @@ function test.line_end() text_area:setCursor(70) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_E') + simulate_input_keys('CUSTOM_END') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -873,7 +873,7 @@ function test.line_end() text_area:setCursor(200) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_E') + simulate_input_keys('CUSTOM_END') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -882,7 +882,7 @@ function test.line_end() '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', }, '\n')); - simulate_input_keys('CUSTOM_CTRL_E') + simulate_input_keys('CUSTOM_END') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -905,7 +905,7 @@ function test.line_beging() simulate_input_text(text) - simulate_input_keys('CUSTOM_CTRL_H') + simulate_input_keys('CUSTOM_HOME') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -917,7 +917,7 @@ function test.line_beging() text_area:setCursor(173) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_H') + simulate_input_keys('CUSTOM_HOME') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -929,7 +929,7 @@ function test.line_beging() text_area:setCursor(1) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_H') + simulate_input_keys('CUSTOM_HOME') expect.eq(read_rendered_text(text_area), table.concat({ '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -1355,10 +1355,10 @@ function test.line_navigation_reset_selection() 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); - simulate_input_keys('CUSTOM_CTRL_H') + simulate_input_keys('CUSTOM_HOME') expect.eq(read_selected_text(text_area), '') - simulate_input_keys('CUSTOM_CTRL_E') + simulate_input_keys('CUSTOM_END') expect.eq(read_selected_text(text_area), '') journal:dismiss() @@ -1496,7 +1496,7 @@ function test.delete_char_delete_selection() 'porttitor mi, vitae rutrum ero', }, '\n')); - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '60: _ metus nec libero.', @@ -2236,7 +2236,7 @@ function test.restore_text_between_sessions() local journal, text_area = arrange_empty_journal({w=80,save_on_change=true}) simulate_input_keys('CUSTOM_CTRL_A') - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') local text = table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -2861,7 +2861,7 @@ function test.fast_rewind_words_right() text_area:setCursor(1) journal:onRender() - simulate_input_keys('A_MOVE_E_DOWN') + simulate_input_keys('CUSTOM_CTRL_RIGHT') expect.eq(read_rendered_text(text_area), table.concat({ '60:_Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -2871,7 +2871,7 @@ function test.fast_rewind_words_right() 'libero.', }, '\n')); - simulate_input_keys('A_MOVE_E_DOWN') + simulate_input_keys('CUSTOM_CTRL_RIGHT') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem_ipsum dolor sit amet, consectetur adipiscing ', @@ -2882,7 +2882,7 @@ function test.fast_rewind_words_right() }, '\n')); for i=1,6 do - simulate_input_keys('A_MOVE_E_DOWN') + simulate_input_keys('CUSTOM_CTRL_RIGHT') end expect.eq(read_rendered_text(text_area), table.concat({ @@ -2893,7 +2893,7 @@ function test.fast_rewind_words_right() 'libero.', }, '\n')); - simulate_input_keys('A_MOVE_E_DOWN') + simulate_input_keys('CUSTOM_CTRL_RIGHT') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -2903,7 +2903,7 @@ function test.fast_rewind_words_right() 'libero.', }, '\n')); - simulate_input_keys('A_MOVE_E_DOWN') + simulate_input_keys('CUSTOM_CTRL_RIGHT') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -2914,7 +2914,7 @@ function test.fast_rewind_words_right() }, '\n')); for i=1,17 do - simulate_input_keys('A_MOVE_E_DOWN') + simulate_input_keys('CUSTOM_CTRL_RIGHT') end expect.eq(read_rendered_text(text_area), table.concat({ @@ -2925,7 +2925,7 @@ function test.fast_rewind_words_right() 'libero._', }, '\n')); - simulate_input_keys('A_MOVE_E_DOWN') + simulate_input_keys('CUSTOM_CTRL_RIGHT') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -2948,7 +2948,7 @@ function test.fast_rewind_words_left() simulate_input_text(text) - simulate_input_keys('A_MOVE_W_DOWN') + simulate_input_keys('CUSTOM_CTRL_LEFT') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -2958,7 +2958,7 @@ function test.fast_rewind_words_left() '_ibero.', }, '\n')); - simulate_input_keys('A_MOVE_W_DOWN') + simulate_input_keys('CUSTOM_CTRL_LEFT') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -2969,7 +2969,7 @@ function test.fast_rewind_words_left() }, '\n')); for i=1,8 do - simulate_input_keys('A_MOVE_W_DOWN') + simulate_input_keys('CUSTOM_CTRL_LEFT') end expect.eq(read_rendered_text(text_area), table.concat({ @@ -2980,7 +2980,7 @@ function test.fast_rewind_words_left() 'libero.', }, '\n')); - simulate_input_keys('A_MOVE_W_DOWN') + simulate_input_keys('CUSTOM_CTRL_LEFT') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -2991,7 +2991,7 @@ function test.fast_rewind_words_left() }, '\n')); for i=1,16 do - simulate_input_keys('A_MOVE_W_DOWN') + simulate_input_keys('CUSTOM_CTRL_LEFT') end expect.eq(read_rendered_text(text_area), table.concat({ @@ -3002,7 +3002,7 @@ function test.fast_rewind_words_left() 'libero.', }, '\n')); - simulate_input_keys('A_MOVE_W_DOWN') + simulate_input_keys('CUSTOM_CTRL_LEFT') expect.eq(read_rendered_text(text_area), table.concat({ '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', @@ -3039,12 +3039,12 @@ function test.fast_rewind_reset_selection() 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); - simulate_input_keys('A_MOVE_W_DOWN') + simulate_input_keys('CUSTOM_CTRL_LEFT') expect.eq(read_selected_text(text_area), '') simulate_input_keys('CUSTOM_CTRL_A') - simulate_input_keys('A_MOVE_E_DOWN') + simulate_input_keys('CUSTOM_CTRL_RIGHT') expect.eq(read_selected_text(text_area), '') journal:dismiss() From efd2b70e6a9edd34013ce1f0bff7fe148d571626 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 14 Sep 2024 13:35:38 -0700 Subject: [PATCH 104/811] also make use of Ctrl-Home and Ctrl-End --- docs/gui/journal.rst | 48 +++++++++++++++++++++----------- internal/journal/text_editor.lua | 4 +-- test/gui/journal.lua | 14 +++++----- 3 files changed, 41 insertions(+), 25 deletions(-) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index 675251e24b..40d824b065 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -30,30 +30,46 @@ Supported Features multiline text input. - Text Wrapping: Text automatically wraps within the editor, ensuring lines fit within the display without manual adjustments. -- Backspace Support: Use the backspace key to delete characters to the left of the cursor. -- Delete Character: :kbd:`Ctrl` + :kbd:`D` deletes the character under the cursor. -- Line Navigation: :kbd:`Ctrl` + :kbd:`H` (like "Home") moves the cursor to the beginning of the current line, and :kbd:`Ctrl` + :kbd:`E` (like "End") moves it to the end. -- Delete Current Line: :kbd:`Ctrl` + :kbd:`U` deletes the entire current line where the cursor is located. -- Delete Rest of Line: :kbd:`Ctrl` + :kbd:`K` deletes text from the cursor to the end of the line. -- Delete Last Word: :kbd:`Ctrl` + :kbd:`W` removes the word immediately before the cursor. -- Text Selection: Select text with the mouse, with support for replacing or removing selected text. -- Jump to Beginning/End: Quickly move the cursor to the beginning or end of the text using :kbd:`Shift` + :kbd:`Up` and :kbd:`Shift` + :kbd:`Down`. -- Select Word/Line: Use double click to select current word, or triple click to select current line +- Backspace Support: Use the backspace key to delete characters to the left of + the cursor. +- Delete Character: :kbd:`Delete` deletes the character under the cursor. +- Line Navigation: :kbd:`Home` moves the cursor to the beginning of the current + line, and :kbd:`End` moves it to the end. +- Delete Current Line: :kbd:`Ctrl` + :kbd:`U` deletes the entire current line + where the cursor is located. +- Delete Rest of Line: :kbd:`Ctrl` + :kbd:`K` deletes text from the cursor to + the end of the line. +- Delete Last Word: :kbd:`Ctrl` + :kbd:`W` removes the word immediately before + the cursor. +- Text Selection: Select text with the mouse, with support for replacing or + removing selected text. +- Jump to Beginning/End: Quickly move the cursor to the beginning or end of the + text using :kbd:`Ctrl` + :kbd:`Home` and :kbd:`Ctrl` + :kbd:`End`. +- Select Word/Line: Use double click to select current word, or triple click to + select current line - Select All: Select entire text by :kbd:`Ctrl` + :kbd:`A` -- Undo/Redo: Undo/Redo changes by :kbd:`Ctrl` + :kbd:`Z` / :kbd:`Ctrl` + :kbd:`Y` -- Clipboard Operations: Perform OS clipboard cut, copy, and paste operations on selected text, allowing you to paste the copied content into other applications. +- Undo/Redo: Undo/Redo changes by :kbd:`Ctrl` + :kbd:`Z` / :kbd:`Ctrl` + + :kbd:`Y` +- Clipboard Operations: Perform OS clipboard cut, copy, and paste operations on + selected text, allowing you to paste the copied content into other + applications. - Copy Text: Use :kbd:`Ctrl` + :kbd:`C` to copy selected text. - copy selected text, if available - - If no text is selected it copy the entire current line, including the terminating newline if present. + - If no text is selected it copy the entire current line, including the + terminating newline if present. - Cut Text: Use :kbd:`Ctrl` + :kbd:`X` to cut selected text. - cut selected text, if available - - If no text is selected it will cut the entire current line, including the terminating newline if present -- Paste Text: Use :kbd:`Ctrl` + :kbd:`V` to paste text from the clipboard into the editor. + - If no text is selected it will cut the entire current line, including the + terminating newline if present +- Paste Text: Use :kbd:`Ctrl` + :kbd:`V` to paste text from the clipboard into + the editor. - replace selected text, if available - If no text is selected, paste text in the cursor position - Scrolling behaviour for long text build-in -- Table of contents (:kbd:`Ctrl` + :kbd:`O`), with headers line prefixed by '#', e.g. '# Fort history', '## Year 1' -- Table of contents navigation: jump to previous/next section by :kbd:`Ctrl` + :kbd:`Up` / :kbd:`Ctrl` + :kbd:`Down` +- Table of contents (:kbd:`Ctrl` + :kbd:`O`), with headers line prefixed by + ``#``, e.g. ``# Fort history``, ``## Year 1`` +- Table of contents navigation: jump to previous/next section by :kbd:`Ctrl` + + :kbd:`Up` / :kbd:`Ctrl` + :kbd:`Down` Usage ----- diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 030668714f..9815a2f12c 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -761,10 +761,10 @@ function TextEditorView:onCursorInput(keys) self:setCursor(offset) self.last_cursor_x = last_cursor_x return true - elseif keys.KEYBOARD_CURSOR_UP_FAST then + elseif keys.CUSTOM_CTRL_HOME then self:setCursor(1) return true - elseif keys.KEYBOARD_CURSOR_DOWN_FAST then + elseif keys.CUSTOM_CTRL_END then -- go to text end self:setCursor(#self.text + 1) return true diff --git a/test/gui/journal.lua b/test/gui/journal.lua index b73a37a9a2..612e860785 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -1110,7 +1110,7 @@ function test.jump_to_text_end() text_area:setCursor(1) journal:onRender() - simulate_input_keys('KEYBOARD_CURSOR_DOWN_FAST') + simulate_input_keys('CUSTOM_CTRL_END') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -1119,7 +1119,7 @@ function test.jump_to_text_end() '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', }, '\n')); - simulate_input_keys('KEYBOARD_CURSOR_DOWN_FAST') + simulate_input_keys('CUSTOM_CTRL_END') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -1142,7 +1142,7 @@ function test.jump_to_text_begin() simulate_input_text(text) - simulate_input_keys('KEYBOARD_CURSOR_UP_FAST') + simulate_input_keys('CUSTOM_CTRL_HOME') expect.eq(read_rendered_text(text_area), table.concat({ '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -1151,7 +1151,7 @@ function test.jump_to_text_begin() '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', }, '\n')); - simulate_input_keys('KEYBOARD_CURSOR_UP_FAST') + simulate_input_keys('CUSTOM_CTRL_HOME') expect.eq(read_rendered_text(text_area), table.concat({ '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -1388,10 +1388,10 @@ function test.jump_begin_or_end_reset_selection() 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); - simulate_input_keys('KEYBOARD_CURSOR_UP_FAST') + simulate_input_keys('CUSTOM_CTRL_HOME') expect.eq(read_selected_text(text_area), '') - simulate_input_keys('KEYBOARD_CURSOR_DOWN_FAST') + simulate_input_keys('CUSTOM_CTRL_END') expect.eq(read_selected_text(text_area), '') journal:dismiss() @@ -2426,7 +2426,7 @@ function test.scroll_follows_cursor() 'Ut gravida tortor ac accumsan suscipit.', }, '\n')) - simulate_input_keys('KEYBOARD_CURSOR_UP_FAST') + simulate_input_keys('CUSTOM_CTRL_HOME') simulate_mouse_click(text_area, 0, 9) simulate_input_keys('KEYBOARD_CURSOR_DOWN') From 663471c49004bbbc47d602c396dedc74a8eaec39 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 14 Sep 2024 13:40:13 -0700 Subject: [PATCH 105/811] formattttiiinnngg --- docs/gui/journal.rst | 56 ++++++++++++++++++++++---------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index 40d824b065..a13bda3e67 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -19,57 +19,57 @@ Supported Features ------------------ - Cursor Control: Navigate through text using arrow keys (Left, Right, Up, - and Down) for precise cursor placement. + and Down) for precise cursor placement. - Fast Rewind: Use :kbd:`Ctrl` + :kbd:`Left` and :kbd:`Ctrl` + :kbd:`Right` to - move the cursor one word back or forward. + move the cursor one word back or forward. - Longest X Position Memory: The cursor remembers the longest x position when - moving up or down, making vertical navigation more intuitive. + moving up or down, making vertical navigation more intuitive. - Mouse Control: Use the mouse to position the cursor within the text, - providing an alternative to keyboard navigation. + providing an alternative to keyboard navigation. - New Lines: Easily insert new lines using the :kbd:`Enter` key, supporting - multiline text input. + multiline text input. - Text Wrapping: Text automatically wraps within the editor, ensuring lines fit - within the display without manual adjustments. + within the display without manual adjustments. - Backspace Support: Use the backspace key to delete characters to the left of - the cursor. + the cursor. - Delete Character: :kbd:`Delete` deletes the character under the cursor. - Line Navigation: :kbd:`Home` moves the cursor to the beginning of the current - line, and :kbd:`End` moves it to the end. + line, and :kbd:`End` moves it to the end. - Delete Current Line: :kbd:`Ctrl` + :kbd:`U` deletes the entire current line - where the cursor is located. + where the cursor is located. - Delete Rest of Line: :kbd:`Ctrl` + :kbd:`K` deletes text from the cursor to - the end of the line. + the end of the line. - Delete Last Word: :kbd:`Ctrl` + :kbd:`W` removes the word immediately before - the cursor. + the cursor. - Text Selection: Select text with the mouse, with support for replacing or - removing selected text. + removing selected text. - Jump to Beginning/End: Quickly move the cursor to the beginning or end of the - text using :kbd:`Ctrl` + :kbd:`Home` and :kbd:`Ctrl` + :kbd:`End`. + text using :kbd:`Ctrl` + :kbd:`Home` and :kbd:`Ctrl` + :kbd:`End`. - Select Word/Line: Use double click to select current word, or triple click to - select current line + select current line - Select All: Select entire text by :kbd:`Ctrl` + :kbd:`A` - Undo/Redo: Undo/Redo changes by :kbd:`Ctrl` + :kbd:`Z` / :kbd:`Ctrl` + - :kbd:`Y` + :kbd:`Y` - Clipboard Operations: Perform OS clipboard cut, copy, and paste operations on - selected text, allowing you to paste the copied content into other - applications. + selected text, allowing you to paste the copied content into other + applications. - Copy Text: Use :kbd:`Ctrl` + :kbd:`C` to copy selected text. - - copy selected text, if available - - If no text is selected it copy the entire current line, including the - terminating newline if present. + - copy selected text, if available + - If no text is selected it copy the entire current line, including the + terminating newline if present. - Cut Text: Use :kbd:`Ctrl` + :kbd:`X` to cut selected text. - - cut selected text, if available - - If no text is selected it will cut the entire current line, including the - terminating newline if present + - cut selected text, if available + - If no text is selected it will cut the entire current line, including the + terminating newline if present - Paste Text: Use :kbd:`Ctrl` + :kbd:`V` to paste text from the clipboard into - the editor. - - replace selected text, if available - - If no text is selected, paste text in the cursor position + the editor. + - replace selected text, if available + - If no text is selected, paste text in the cursor position - Scrolling behaviour for long text build-in - Table of contents (:kbd:`Ctrl` + :kbd:`O`), with headers line prefixed by - ``#``, e.g. ``# Fort history``, ``## Year 1`` + ``#``, e.g. ``# Fort history``, ``## Year 1`` - Table of contents navigation: jump to previous/next section by :kbd:`Ctrl` + - :kbd:`Up` / :kbd:`Ctrl` + :kbd:`Down` + :kbd:`Up` / :kbd:`Ctrl` + :kbd:`Down` Usage ----- From f239c68fa60e6498a423ec9408172058ace2df09 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 14 Sep 2024 20:24:12 -0700 Subject: [PATCH 106/811] Apply suggestions from code review --- changelog.txt | 1 - docs/idle-crafting.rst | 2 +- idle-crafting.lua | 4 ++-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index 30ace4473a..ae1b4851e7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -57,7 +57,6 @@ Template for new versions: - `position`: option to copy keyboard cursor position to the clipboard - `assign-minecarts`: reassign vehicles to routes where the vehicle has been destroyed (or has otherwise gone missing) - `fix/dry-buckets`: prompt DF to recheck requests for aid (e.g. "bring water" jobs) when a bucket is unclogged and becomes available for use -- `idle-crafting`: make choice of crafting job dependent on resources in linked stockpiles. - `exterminate`: show descriptive names for the listed races in addition to their IDs ## Documentation diff --git a/docs/idle-crafting.rst b/docs/idle-crafting.rst index d1cd23c512..5f99d24df6 100644 --- a/docs/idle-crafting.rst +++ b/docs/idle-crafting.rst @@ -46,7 +46,7 @@ this way. When a workshop is designated for idle crafting, this tool will create crafting jobs and assign them to idle dwarves who have a need for crafting objects. This -script respects the setting for permitted general work orders from the "Workers" +script respects the setting for permitted general work order labors from the "Workers" tab. For workshops without input stockpile links, bone carving and stonecrafting are diff --git a/idle-crafting.lua b/idle-crafting.lua index 92673f51b8..2ccf1d30f4 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -15,7 +15,7 @@ local function for_inputs(workshop, action) else for _, stockpile in ipairs(workshop.profile.links.take_from_pile) do for _, item in ipairs(dfhack.buildings.getStockpileContents(stockpile)) do - if(item:isAssignedToThisStockpile(stockpile.id)) then + if item:isAssignedToThisStockpile(stockpile.id) then for _, contained_item in ipairs(dfhack.items.getContainedItems(item)) do action(contained_item) end @@ -25,7 +25,7 @@ local function for_inputs(workshop, action) end end for _, contained_item in ipairs(workshop.contained_items) do - if contained_item.use_mode == 0 then + if contained_item.use_mode == df.building_item_role_type.TEMP then action(contained_item.item) end end From bcc304b9f5a37b4977cc2edb312cfec0f3148805 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 14 Sep 2024 23:55:02 -0700 Subject: [PATCH 107/811] changelog editing pass --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index cd5d2baba1..63e2121c7c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,7 +30,7 @@ Template for new versions: - `embark-anyone`: allows you to embark as any civilization, including dead and non-dwarven civs - `idle-crafting`: allow dwarves to independently satisfy their need to craft objects - `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationships -- `notes`: manage map-specific notes +- `notes`: attach notes to locations on a fort map ## New Features - `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them From 46a96763cad623d95fc57d6a1c7eb8c96249286b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 15 Sep 2024 00:59:51 -0700 Subject: [PATCH 108/811] bump changelog to 50.13-r5 --- changelog.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 63e2121c7c..ff109f1319 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,18 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 50.13-r5 + ## New Tools - `embark-anyone`: allows you to embark as any civilization, including dead and non-dwarven civs - `idle-crafting`: allow dwarves to independently satisfy their need to craft objects @@ -64,8 +76,6 @@ Template for new versions: ## Documentation - `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses -## Removed - # 50.13-r4 ## New Features From ad1e31fb13627453ee50c32bf8be76a5a77f4b85 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 16 Sep 2024 19:28:03 -0700 Subject: [PATCH 109/811] use find methods instead of array indexing because the vector indices are not guaranteed to be the same as the ids (though it appears that they usually are) --- necronomicon.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/necronomicon.lua b/necronomicon.lua index 68ef8d083c..13eb6fdeb8 100644 --- a/necronomicon.lua +++ b/necronomicon.lua @@ -14,7 +14,7 @@ function get_book_interactions(item) for _, ref in ipairs (written_content.refs) do if ref._type == df.general_ref_interactionst then - local interaction = df.global.world.raws.interactions[ref.interaction_id] + local interaction = df.interaction.find(ref.interaction_id) table.insert(book_interactions, interaction) end end @@ -34,7 +34,7 @@ end function get_item_artifact(item) for _, ref in ipairs(item.general_refs) do if ref._type == df.general_ref_is_artifactst then - return df.global.world.artifacts.all[ref.artifact_id] + return df.artifact_record.find(ref.artifact_id) end end end From 0602cbb0c7f86a4172686ec6bc7541d4ec49d0e7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 16 Sep 2024 20:21:17 -0700 Subject: [PATCH 110/811] remove author tag (it's available in git blame) --- necronomicon.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/necronomicon.lua b/necronomicon.lua index 13eb6fdeb8..fcffb0fdb0 100644 --- a/necronomicon.lua +++ b/necronomicon.lua @@ -1,5 +1,4 @@ -- lists books that contain secrets of life and death. --- Author: Ajhaa local argparse = require("argparse") From 8ad4fa10b8f0b129dde67a97ef58201025411450 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 19 Sep 2024 07:34:26 +0200 Subject: [PATCH 111/811] Polishing gui notes --- gui/notes.lua | 7 +++++-- internal/notes/note_manager.lua | 1 - 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index d6354ac1e3..b0c263e12b 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -79,6 +79,9 @@ function NotesWindow:init() frame={l=0,b=2}, frame_inset={t=1}, row_height=1, + on_select=function (ind, note) + self:loadNote(note) + end, on_submit=function (ind, note) self:loadNote(note) dfhack.gui.pauseRecenter(note.point.pos) @@ -300,9 +303,9 @@ function NotesScreen:onInput(keys) local manager = note_manager.NoteManager{ note=nil, on_update=function() - self.subviews.notes_window:reloadFilteredNotes() dfhack.run_command_silent('overlay trigger notes.map_notes') - self:dismiss() + self.subviews.notes_window:reloadFilteredNotes() + self:stopNoteAdd() end, on_dismiss=function() self:stopNoteAdd() diff --git a/internal/notes/note_manager.lua b/internal/notes/note_manager.lua index 8d33bcc16c..5affb69b62 100644 --- a/internal/notes/note_manager.lua +++ b/internal/notes/note_manager.lua @@ -54,7 +54,6 @@ function NoteManager:init() frame={t=6,b=3}, frame_style=gui.FRAME_INTERIOR, init_text=self.note and self.note.point.comment or '', - -- init_cursor=1 }, widgets.Panel{ view_id='buttons', From 5742e4e4d3209a174061d6b4aea653887efe2a99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 19 Sep 2024 19:01:57 +0200 Subject: [PATCH 112/811] Improve gui/notes and gui/journal keyboard control --- docs/gui/journal.rst | 4 ++-- docs/notes.rst | 2 +- gui/notes.lua | 16 +++++---------- internal/journal/text_editor.lua | 6 +++--- internal/notes/note_manager.lua | 6 +++--- test/gui/journal.lua | 34 ++++++++++++++++---------------- 6 files changed, 31 insertions(+), 37 deletions(-) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index d6004ec95d..0063ff63ad 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -26,8 +26,8 @@ Supported Features - New Lines: Easily insert new lines using the :kbd:`Enter` key, supporting multiline text input. - Text Wrapping: Text automatically wraps within the editor, ensuring lines fit within the display without manual adjustments. - Backspace Support: Use the backspace key to delete characters to the left of the cursor. -- Delete Character: :kbd:`Ctrl` + :kbd:`D` deletes the character under the cursor. -- Line Navigation: :kbd:`Ctrl` + :kbd:`H` (like "Home") moves the cursor to the beginning of the current line, and :kbd:`Ctrl` + :kbd:`E` (like "End") moves it to the end. +- Delete Character: :kbd:`Delete` deletes the character under the cursor. +- Line Navigation: :kbd:`Home` moves the cursor to the beginning of the current line, and :kbd:`End` moves it to the end. - Delete Current Line: :kbd:`Ctrl` + :kbd:`U` deletes the entire current line where the cursor is located. - Delete Rest of Line: :kbd:`Ctrl` + :kbd:`K` deletes text from the cursor to the end of the line. - Delete Last Word: :kbd:`Ctrl` + :kbd:`W` removes the word immediately before the cursor. diff --git a/docs/notes.rst b/docs/notes.rst index faed2c8fbf..19e169428e 100644 --- a/docs/notes.rst +++ b/docs/notes.rst @@ -29,7 +29,7 @@ Creating a Note 1. Use the keyboard cursor to select the desired map tile where you want to place a note. 2. Execute ``notes add`` via the DFHack console. 3. In the pop-up dialog, fill in the note's title and detailed comment. -4. Press :kbd:`Alt` + :kbd:`S` to create the note. +4. Press :kbd:`Ctrl` + :kbd:`S` to create the note. Editing or Deleting a Note -------------------------- diff --git a/gui/notes.lua b/gui/notes.lua index b0c263e12b..b649356f05 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -57,13 +57,6 @@ function NotesWindow:init() frame_inset={l=1,t=1,b=1,r=1}, autoarrange_subviews=true, subviews={ - widgets.HotkeyLabel { - key='CUSTOM_ALT_S', - label='Search', - frame={l=0}, - auto_width=true, - on_activate=function() self.subviews.search:setFocus(true) end, - }, NotesSearchField{ view_id='search', frame={l=0,h=3}, @@ -94,7 +87,7 @@ function NotesWindow:init() frame={l=1,b=1,h=1}, auto_width=true, label='New note', - key='CUSTOM_ALT_N', + key='CUSTOM_CTRL_N', visible=edit_mode, on_activate=function() if self.on_note_add then @@ -151,7 +144,7 @@ function NotesWindow:init() frame={l=0,t=0,h=1}, auto_width=true, label='Edit', - key='CUSTOM_ALT_U', + key='CUSTOM_CTRL_E', on_activate=function() self:showNoteManager(self.selected_note) end, }, widgets.HotkeyLabel{ @@ -159,7 +152,7 @@ function NotesWindow:init() frame={r=0,t=0,h=1}, auto_width=true, label='Delete', - key='CUSTOM_ALT_D', + key='CUSTOM_CTRL_D', on_activate=function() self:deleteNote(self.selected_note) end, }, } @@ -357,7 +350,8 @@ function NotesScreen:onRenderFrame(dc, rect) end function NotesScreen:onAboutToShow() - if not overlay.get_state().config[OVERLAY_NAME].enabled then + local notes_overlay = overlay.get_state().config[OVERLAY_NAME] + if notes_overlay and not notes_overlay.enabled then self.should_disable_overlay = true overlay.overlay_command({'enable', 'notes.map_notes'}) end diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index b5da3950b6..abbca900eb 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -786,13 +786,13 @@ function TextEditorView:onCursorInput(keys) local word_end = self:wordEndOffset() self:setCursor(word_end) return true - elseif keys.CUSTOM_CTRL_H then + elseif keys.CUSTOM_HOME then -- line start self:setCursor( self:lineStartOffset() ) return true - elseif keys.CUSTOM_CTRL_E then + elseif keys.CUSTOM_END then -- line end self:setCursor( self:lineEndOffset() @@ -890,7 +890,7 @@ function TextEditorView:onTextManipulationInput(keys) self:eraseSelection() return true - elseif keys.CUSTOM_CTRL_D then + elseif keys.CUSTOM_DELETE then -- delete char, there is no support for `Delete` key self.history:store(HISTORY_ENTRY.DELETE, self.text, self.cursor) diff --git a/internal/notes/note_manager.lua b/internal/notes/note_manager.lua index 5affb69b62..b7a8d8059a 100644 --- a/internal/notes/note_manager.lua +++ b/internal/notes/note_manager.lua @@ -65,7 +65,7 @@ function NoteManager:init() frame={l=0,t=0,h=1}, auto_width=true, label='Save', - key='CUSTOM_ALT_S', + key='CUSTOM_CTRL_S', visible=edit_mode, on_activate=function() self:saveNote() end, enabled=function() return #self.subviews.name:getText() > 0 end, @@ -75,7 +75,7 @@ function NoteManager:init() frame={l=0,t=0,h=1}, auto_width=true, label='Create', - key='CUSTOM_ALT_S', + key='CUSTOM_CTRL_S', visible=not edit_mode, on_activate=function() self:createNote() end, enabled=function() return #self.subviews.name:getText() > 0 end, @@ -85,7 +85,7 @@ function NoteManager:init() frame={r=0,t=0,h=1}, auto_width=true, label='Delete', - key='CUSTOM_ALT_D', + key='CUSTOM_CTRL_D', visible=edit_mode, on_activate=function() self:deleteNote() end, }, diff --git a/test/gui/journal.lua b/test/gui/journal.lua index d845aef20e..d86e415d10 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -772,7 +772,7 @@ function test.handle_delete() text_area:setCursor(1) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '_: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -783,7 +783,7 @@ function test.handle_delete() text_area:setCursor(124) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -794,7 +794,7 @@ function test.handle_delete() text_area:setCursor(123) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -805,7 +805,7 @@ function test.handle_delete() text_area:setCursor(171) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -815,7 +815,7 @@ function test.handle_delete() }, '\n')); for i=1,59 do - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') end expect.eq(read_rendered_text(text_area), table.concat({ @@ -824,7 +824,7 @@ function test.handle_delete() 'nibhorttitor mi, vitae rutrum eros metus nec libero._', }, '\n')); - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -849,7 +849,7 @@ function test.line_end() text_area:setCursor(1) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_E') + simulate_input_keys('CUSTOM_END') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', @@ -861,7 +861,7 @@ function test.line_end() text_area:setCursor(70) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_E') + simulate_input_keys('CUSTOM_END') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -873,7 +873,7 @@ function test.line_end() text_area:setCursor(200) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_E') + simulate_input_keys('CUSTOM_END') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -882,7 +882,7 @@ function test.line_end() '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', }, '\n')); - simulate_input_keys('CUSTOM_CTRL_E') + simulate_input_keys('CUSTOM_END') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -905,7 +905,7 @@ function test.line_beging() simulate_input_text(text) - simulate_input_keys('CUSTOM_CTRL_H') + simulate_input_keys('CUSTOM_HOME') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -917,7 +917,7 @@ function test.line_beging() text_area:setCursor(173) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_H') + simulate_input_keys('CUSTOM_HOME') expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -929,7 +929,7 @@ function test.line_beging() text_area:setCursor(1) journal:onRender() - simulate_input_keys('CUSTOM_CTRL_H') + simulate_input_keys('CUSTOM_HOME') expect.eq(read_rendered_text(text_area), table.concat({ '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -1355,10 +1355,10 @@ function test.line_navigation_reset_selection() 'porttitor mi, vitae rutrum eros metus nec libero.', }, '\n')); - simulate_input_keys('CUSTOM_CTRL_H') + simulate_input_keys('CUSTOM_HOME') expect.eq(read_selected_text(text_area), '') - simulate_input_keys('CUSTOM_CTRL_E') + simulate_input_keys('CUSTOM_END') expect.eq(read_selected_text(text_area), '') journal:dismiss() @@ -1496,7 +1496,7 @@ function test.delete_char_delete_selection() 'porttitor mi, vitae rutrum ero', }, '\n')); - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') expect.eq(read_rendered_text(text_area), table.concat({ '60: _ metus nec libero.', @@ -2236,7 +2236,7 @@ function test.restore_text_between_sessions() local journal, text_area = arrange_empty_journal({w=80,save_on_change=true}) simulate_input_keys('CUSTOM_CTRL_A') - simulate_input_keys('CUSTOM_CTRL_D') + simulate_input_keys('CUSTOM_DELETE') local text = table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', From 9f33bebf3a8f9943df86d6b933d3c6c2eef40d6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 19 Sep 2024 19:11:44 +0200 Subject: [PATCH 113/811] Disable up/down control in text field in one-line-mode --- gui/notes.lua | 14 +------------- internal/journal/text_editor.lua | 7 +++++++ 2 files changed, 8 insertions(+), 13 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index b649356f05..9ec7234fe0 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -23,18 +23,6 @@ local green_pin = dfhack.textures.loadTileset( true ) -NotesSearchField = defclass(NotesSearchField, text_editor.TextEditor) -NotesSearchField.ATTRS {} - -function NotesSearchField:onInput(keys) - -- allow cursor up/down to be used to navigate the notes list - if keys.KEYBOARD_CURSOR_UP or keys.KEYBOARD_CURSOR_DOWN then - return false - end - - return NotesSearchField.super.onInput(self, keys) -end - NotesWindow = defclass(NotesWindow, widgets.Window) NotesWindow.ATTRS { frame_title='DF Notes', @@ -57,7 +45,7 @@ function NotesWindow:init() frame_inset={l=1,t=1,b=1,r=1}, autoarrange_subviews=true, subviews={ - NotesSearchField{ + text_editor.TextEditor{ view_id='search', frame={l=0,h=3}, frame_style=gui.FRAME_INTERIOR, diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index abbca900eb..e7164c463d 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -752,6 +752,9 @@ function TextEditorView:onCursorInput(keys) self:setCursor(self.cursor + 1) return true elseif keys.KEYBOARD_CURSOR_UP then + if self.one_line_mode then + return false + end local x, y = self.wrapped_text:indexToCoords(self.cursor) local last_cursor_x = self.last_cursor_x or x local offset = y > 1 and @@ -761,6 +764,10 @@ function TextEditorView:onCursorInput(keys) self.last_cursor_x = last_cursor_x return true elseif keys.KEYBOARD_CURSOR_DOWN then + if self.one_line_mode then + return false + end + local x, y = self.wrapped_text:indexToCoords(self.cursor) local last_cursor_x = self.last_cursor_x or x local offset = y < #self.wrapped_text.lines and From 1998f766b13b122295b454fb7aa9a89dca7c57bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 19 Sep 2024 20:53:23 +0200 Subject: [PATCH 114/811] Migrate note manager confirmation to ctrl+enter --- docs/notes.rst | 2 +- internal/notes/note_manager.lua | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/notes.rst b/docs/notes.rst index 19e169428e..a67e05b37c 100644 --- a/docs/notes.rst +++ b/docs/notes.rst @@ -29,7 +29,7 @@ Creating a Note 1. Use the keyboard cursor to select the desired map tile where you want to place a note. 2. Execute ``notes add`` via the DFHack console. 3. In the pop-up dialog, fill in the note's title and detailed comment. -4. Press :kbd:`Ctrl` + :kbd:`S` to create the note. +4. Press :kbd:`Ctrl` + :kbd:`Enter` to create the note. Editing or Deleting a Note -------------------------- diff --git a/internal/notes/note_manager.lua b/internal/notes/note_manager.lua index b7a8d8059a..9df64d9694 100644 --- a/internal/notes/note_manager.lua +++ b/internal/notes/note_manager.lua @@ -65,7 +65,7 @@ function NoteManager:init() frame={l=0,t=0,h=1}, auto_width=true, label='Save', - key='CUSTOM_CTRL_S', + key='CUSTOM_CTRL_ENTER', visible=edit_mode, on_activate=function() self:saveNote() end, enabled=function() return #self.subviews.name:getText() > 0 end, @@ -75,7 +75,7 @@ function NoteManager:init() frame={l=0,t=0,h=1}, auto_width=true, label='Create', - key='CUSTOM_CTRL_S', + key='CUSTOM_CTRL_ENTER', visible=not edit_mode, on_activate=function() self:createNote() end, enabled=function() return #self.subviews.name:getText() > 0 end, From aff2ab8a87300c1d1e0f1375b26e7e0f345632a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 19 Sep 2024 20:53:58 +0200 Subject: [PATCH 115/811] Make `gui/notes` respect the global search settings --- gui/notes.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 9ec7234fe0..09e7319249 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -3,9 +3,11 @@ local gui = require 'gui' local widgets = require 'gui.widgets' -local guidm = require('gui.dwarfmode') +local guidm = require 'gui.dwarfmode' local script = require 'gui.script' local overlay = require 'plugins.overlay' +local utils = require 'utils' + local text_editor = reqscript('internal/journal/text_editor') local note_manager = reqscript('internal/notes/note_manager') @@ -228,10 +230,9 @@ function NotesWindow:loadFilteredNotes(search_phrase, force) return end - local point_name_lowercase = map_point.name:lower() if ( - point_name_lowercase ~= nil and #point_name_lowercase > 0 and - point_name_lowercase:find(search_phrase) + #map_point.name > 0 and + utils.search_text(map_point.name, search_phrase) ) then table.insert(choices, { text=map_point.name, From 76c70e780e1d29608c1b2ee9a5f27c8abe2f792a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 20 Sep 2024 19:28:43 +0200 Subject: [PATCH 116/811] Make gui/notes search for any length of search text --- gui/notes.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 09e7319249..309130f97f 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -208,9 +208,6 @@ function NotesWindow:loadFilteredNotes(search_phrase, force) local full_list_loaded = self.curr_search_phrase == '' search_phrase = search_phrase:lower() - if #search_phrase < 3 then - search_phrase = '' - end self.curr_search_phrase = search_phrase From 53e25d5eca34c869c25daed71bf04d888afdd646 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 21 Sep 2024 04:11:24 -0700 Subject: [PATCH 117/811] Update deep-embark.lua getUnitsInBox now only returns active units --- deep-embark.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deep-embark.lua b/deep-embark.lua index 1745774a9b..dba12075cf 100644 --- a/deep-embark.lua +++ b/deep-embark.lua @@ -161,7 +161,7 @@ function moveEmbarkStuff(selectedBlock, embarkTiles) local unitsAtSpawn = dfhack.units.getUnitsInBox(x1, y1, z1, x2, y2, z2) local movedUnit = false for i, unit in ipairs(unitsAtSpawn) do - if unit.civ_id == df.global.plotinfo.civ_id and not unit.flags1.inactive and not unit.flags2.killed then + if unit.civ_id == df.global.plotinfo.civ_id and not unit.flags2.killed then local pos = embarkTiles[math.random(1, #embarkTiles)] dfhack.units.teleport(unit, pos) reveal(pos) From e50fc2ed99479e1268b3421af4e929cd15cb325b Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 21 Sep 2024 04:32:02 -0700 Subject: [PATCH 118/811] Update reaction-trigger.lua Inactive units shouldn't use range if anybody updates this tool. --- modtools/reaction-trigger.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modtools/reaction-trigger.lua b/modtools/reaction-trigger.lua index c71e84f378..5514a01513 100644 --- a/modtools/reaction-trigger.lua +++ b/modtools/reaction-trigger.lua @@ -181,7 +181,7 @@ local validArgs = utils.invert({ 'allowMultipleTargets', 'range', 'ignoreWorker', - 'dontSkipInactive', + 'dontSkipInactive', --TODO: positions for inactive units are meaningless! 'resetPolicy' }) From b83d395940041d41a6bb7c31ee394c7404065e6f Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 21 Sep 2024 04:33:49 -0700 Subject: [PATCH 119/811] Update stuckdoors.lua getUnitsInBox now returns active by default --- fix/stuckdoors.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fix/stuckdoors.lua b/fix/stuckdoors.lua index 8e1e1bc026..ec48a847b5 100644 --- a/fix/stuckdoors.lua +++ b/fix/stuckdoors.lua @@ -9,7 +9,7 @@ end -- Util function: find out if there are any units on the tile with coordinates x,y,z function unitOnTile(x, y, z) - local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z,dfhack.units.isActive) + local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z) return #(units) > 0 end From c39ab02d9f1d33dfd79949894d6696b9b2d621d7 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 21 Sep 2024 04:35:16 -0700 Subject: [PATCH 120/811] Update exterminate.rst Stray punctuation --- docs/exterminate.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/exterminate.rst b/docs/exterminate.rst index 664019aec2..9ce66b9791 100644 --- a/docs/exterminate.rst +++ b/docs/exterminate.rst @@ -43,7 +43,7 @@ Options ``-m``, ``--method `` Specifies the "method" of killing units. See below for details. ``-o``, ``--only-visible`` - Specifies the tool should only kill units visible to the player. + Specifies the tool should only kill units visible to the player on the map. ``-f``, ``--include-friendly`` Specifies the tool should also kill units friendly to the player. From 41128cafce5ac4e8899d2a943599719eba0ea6d2 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 21 Sep 2024 04:36:29 -0700 Subject: [PATCH 121/811] Update position.rst - Tag dfhack --- docs/position.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/position.rst b/docs/position.rst index 23a8cfe1e0..9d06d39618 100644 --- a/docs/position.rst +++ b/docs/position.rst @@ -3,7 +3,7 @@ position .. dfhack-tool:: :summary: Report cursor and mouse position, along with other info. - :tags: adventure fort inspection map + :tags: adventure dfhack fort inspection map This tool reports the current date, clock time, month, season, and historical era. It also reports the keyboard cursor position (or just the z-level if no From 465213a03919ddee2e53602523e9d27d8e393269 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 24 Sep 2024 14:49:56 -0700 Subject: [PATCH 122/811] Silence stuck-worship.lua spam --- fix/stuck-worship.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/fix/stuck-worship.lua b/fix/stuck-worship.lua index 0781181f5f..1531af139a 100644 --- a/fix/stuck-worship.lua +++ b/fix/stuck-worship.lua @@ -1,3 +1,5 @@ +debug_print = false + local function for_pray_need(needs, fn) for idx, need in ipairs(needs) do if need.id == df.need_type.PrayOrMeditate then @@ -85,7 +87,7 @@ for _,unit in ipairs(dfhack.units.getCitizens(false, true)) do goto next_unit end local needs = unit.status.current_soul.personality.needs - if shuffle_prayer_needs(needs, prayer_targets) then + if shuffle_prayer_needs(needs, prayer_targets) and debug_print then print('rebalanced prayer needs for ' .. dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit)))) end From 2c4feaaf104504041d6494d0217a73a85a5b3a76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Sat, 28 Sep 2024 10:12:38 +0200 Subject: [PATCH 123/811] Add realistic-melting to control panel --- internal/control-panel/registry.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 5f20954cc2..5e0fbecc03 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -125,6 +125,8 @@ COMMANDS_BY_IDX = { {command='partial-items', help_command='tweak', group='gameplay', mode='tweak', default=true, desc='Displays percentages on partially-consumed items like hospital cloth.'}, {command='pop-control', group='gameplay', mode='enable'}, + {command='realistic-melting', help_command='tweak', group='gameplay', mode='tweak', default=false, + desc='Adjust selected item types melt return for all metals to ~95% of forging cost. Reduce melt return by 10% per wear level. Affects weapons, shields, armor parts, tools, and trap components.'}, {command='starvingdead', group='gameplay', mode='enable'}, {command='timestream', group='gameplay', mode='enable'}, {command='work-now', group='gameplay', mode='enable'}, From c72a344b29b193b73443f62011041094a8468bfc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Sat, 28 Sep 2024 10:24:36 +0200 Subject: [PATCH 124/811] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index ff109f1319..8fa2f82b16 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `control-panel`: Add realistic-melting tweak to control-panel registry ## Removed From ee17b6eb9bed1879370d50e0b85a8fc2a8f8620d Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Fri, 27 Sep 2024 17:19:44 +0200 Subject: [PATCH 125/811] `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles --- changelog.txt | 2 ++ docs/idle-crafting.rst | 8 ++++---- idle-crafting.lua | 29 +++++++++++++++++++++++++++++ 3 files changed, 35 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index ff109f1319..07f2caeea2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,8 @@ Template for new versions: ## Misc Improvements +- `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles + ## Removed # 50.13-r5 diff --git a/docs/idle-crafting.rst b/docs/idle-crafting.rst index 5f99d24df6..b0377a6a75 100644 --- a/docs/idle-crafting.rst +++ b/docs/idle-crafting.rst @@ -54,7 +54,7 @@ supported, with stonecrafting being the default option. Thus, to designate a workshop for bone carving, disable the stonecrafting labor while keeping the bone carving labor enabled. -For workshops with input stockpile links, the creation of totems and horn crafts -are supported as well. In this case, the choice of job is made randomly based on -the resources available in the input stockpiles (respecting the permitted -labors from the workshop profile). +For workshops with input stockpile links, the creation of totems, shell crafts, +and horn crafts are supported as well. In this case, the choice of job is made +randomly based on the resources available in the input stockpiles (respecting +the permitted labors from the workshop profile). diff --git a/idle-crafting.lua b/idle-crafting.lua index fd650c8810..f78e667631 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -143,6 +143,31 @@ function makeBoneCraft(unit, workshop) return dfhack.job.addWorker(job, unit) end +---make shell crafts at specified workshop +---@param unit df.unit +---@param workshop df.building_workshopst +---@return boolean +function makeShellCraft(unit, workshop) + local job = make_job() + job.job_type = df.job_type.MakeCrafts + job.mat_type = -1 + job.material_category.shell = true + + local jitem = df.job_item:new() + jitem.item_type = df.item_type.NONE + jitem.mat_type = -1 + jitem.mat_index = -1 + jitem.quantity = 1 + jitem.vector_id = df.job_item_vector_id.ANY_REFUSE + jitem.flags1.unrotten = true + jitem.flags2.shell = true + jitem.flags2.body_part = true + job.job_items.elements:insert('#', jitem) + + assignToWorkshop(job, workshop) + return dfhack.job.addWorker(job, unit) +end + ---make rock crafts at specified workshop ---@param unit df.unit ---@param workshop df.building_workshopst @@ -177,6 +202,8 @@ local function categorize_craft(tab,item) tab['skull'] = (tab['skull'] or 0) + 1 elseif item.corpse_flags.horn then tab['horn'] = (tab['horn'] or 0) + item.material_amount.Horn + elseif item.corpse_flags.shell then + tab['shell'] = (tab['shell'] or 0) + 1 end elseif df.item_boulderst:is_instance(item) then tab['boulder'] = (tab['boulder'] or 0) + 1 @@ -310,11 +337,13 @@ function select_crafting_job(workshop) tab['bone'] = nil tab['skull'] = nil tab['horn'] = nil + tab['shell'] = nil end local material = weightedChoice(tab) if material == 'bone' then return makeBoneCraft elseif material == 'skull' then return makeTotem elseif material == 'horn' then return makeHornCrafts + elseif material == 'shell' then return makeShellCraft elseif material == 'boulder' then return makeRockCraft else return nil From 73830e25fc2ef1fd2cafc8d905c2d9c92c3e1eb7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Sat, 28 Sep 2024 17:59:09 +0200 Subject: [PATCH 126/811] shortened 'realistic-melting' description for control panel --- internal/control-panel/registry.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 5e0fbecc03..e300177017 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -126,7 +126,7 @@ COMMANDS_BY_IDX = { desc='Displays percentages on partially-consumed items like hospital cloth.'}, {command='pop-control', group='gameplay', mode='enable'}, {command='realistic-melting', help_command='tweak', group='gameplay', mode='tweak', default=false, - desc='Adjust selected item types melt return for all metals to ~95% of forging cost. Reduce melt return by 10% per wear level. Affects weapons, shields, armor parts, tools, and trap components.'}, + desc='Adjust selected item types melt return for all metals to ~95% of forging cost. Reduce melt return by 10% per wear level.'}, {command='starvingdead', group='gameplay', mode='enable'}, {command='timestream', group='gameplay', mode='enable'}, {command='work-now', group='gameplay', mode='enable'}, From 546d6dbf55f0e6c894ec053c783a1bbfa90f10a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ryszard=20Panto=C5=82?= Date: Sat, 28 Sep 2024 18:07:27 +0200 Subject: [PATCH 127/811] removed default=false from control panel registry entry for realistic-melting --- internal/control-panel/registry.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index e300177017..4e0bdd2e67 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -125,7 +125,7 @@ COMMANDS_BY_IDX = { {command='partial-items', help_command='tweak', group='gameplay', mode='tweak', default=true, desc='Displays percentages on partially-consumed items like hospital cloth.'}, {command='pop-control', group='gameplay', mode='enable'}, - {command='realistic-melting', help_command='tweak', group='gameplay', mode='tweak', default=false, + {command='realistic-melting', help_command='tweak', group='gameplay', mode='tweak', desc='Adjust selected item types melt return for all metals to ~95% of forging cost. Reduce melt return by 10% per wear level.'}, {command='starvingdead', group='gameplay', mode='enable'}, {command='timestream', group='gameplay', mode='enable'}, From c1b23c7e504e5a2ad7da7fc952a3612eaf26d092 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 28 Sep 2024 12:17:35 -0700 Subject: [PATCH 128/811] Separate PR * Update stuckdoors.lua * Update deep-embark.lua --- deep-embark.lua | 2 +- fix/stuckdoors.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deep-embark.lua b/deep-embark.lua index dba12075cf..1745774a9b 100644 --- a/deep-embark.lua +++ b/deep-embark.lua @@ -161,7 +161,7 @@ function moveEmbarkStuff(selectedBlock, embarkTiles) local unitsAtSpawn = dfhack.units.getUnitsInBox(x1, y1, z1, x2, y2, z2) local movedUnit = false for i, unit in ipairs(unitsAtSpawn) do - if unit.civ_id == df.global.plotinfo.civ_id and not unit.flags2.killed then + if unit.civ_id == df.global.plotinfo.civ_id and not unit.flags1.inactive and not unit.flags2.killed then local pos = embarkTiles[math.random(1, #embarkTiles)] dfhack.units.teleport(unit, pos) reveal(pos) diff --git a/fix/stuckdoors.lua b/fix/stuckdoors.lua index ec48a847b5..8e1e1bc026 100644 --- a/fix/stuckdoors.lua +++ b/fix/stuckdoors.lua @@ -9,7 +9,7 @@ end -- Util function: find out if there are any units on the tile with coordinates x,y,z function unitOnTile(x, y, z) - local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z) + local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z,dfhack.units.isActive) return #(units) > 0 end From 714df2f8127b5c6d56823c43b4096da1e0d88045 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 28 Sep 2024 13:07:46 -0700 Subject: [PATCH 129/811] deduplicate notes blueprints if run on repeat --- changelog.txt | 1 + internal/quickfort/notes.lua | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index e9a183309c..e93bc153e4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/quickfort`: only print a help blueprint's text once even if the repeat setting is enabled ## Misc Improvements - `control-panel`: Add realistic-melting tweak to control-panel registry diff --git a/internal/quickfort/notes.lua b/internal/quickfort/notes.lua index aa64168607..5667cfb933 100644 --- a/internal/quickfort/notes.lua +++ b/internal/quickfort/notes.lua @@ -31,7 +31,11 @@ function do_run(_, grid, ctx) if #line > 0 then table.insert(lines, table.concat(line, ' ')) end - table.insert(ctx.messages, table.concat(lines, '\n')) + local message = table.concat(lines, '\n') + if not ctx.messages_set[message] then + table.insert(ctx.messages, message) + ctx.messages_set[message] = true + end end function do_orders() From 50f3f8bc1aab617033ca999f617402131cb4ffe8 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sat, 28 Sep 2024 13:59:34 -0700 Subject: [PATCH 130/811] Add verbose option, update docs * Update stuck-worship.lua * Update dry-buckets.rst * Update stuck-worship.rst * Update changelog.txt --- changelog.txt | 1 + docs/fix/dry-buckets.rst | 7 ++++--- docs/fix/stuck-worship.rst | 7 ++++--- fix/stuck-worship.lua | 24 ++++++++++++++++++++---- 4 files changed, 29 insertions(+), 10 deletions(-) diff --git a/changelog.txt b/changelog.txt index ff109f1319..647ad7f854 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `fix/stuck-worship`: reduced console output by default. ``--verbose`` option to print all affected units. ## Removed diff --git a/docs/fix/dry-buckets.rst b/docs/fix/dry-buckets.rst index 9d3a7d4e0b..65740e309d 100644 --- a/docs/fix/dry-buckets.rst +++ b/docs/fix/dry-buckets.rst @@ -19,6 +19,7 @@ manually. Usage ----- -:: - - fix/dry-buckets +``fix/dry-buckets`` + Empty water buckets not currently used in jobs. +``fix/dry-buckets -q``, ``fix/dry-buckets --quiet`` + Empty water buckets not currently used in jobs. Don't print to the console. diff --git a/docs/fix/stuck-worship.rst b/docs/fix/stuck-worship.rst index b0dbfe07fe..c4bccc0554 100644 --- a/docs/fix/stuck-worship.rst +++ b/docs/fix/stuck-worship.rst @@ -26,6 +26,7 @@ another task. Usage ----- -:: - - fix/stuck-worship +``fix/stuck-worship`` + Rebalance prayer needs of units in the fort. +``fix/stuck-worship -v``, ``fix/stuck-worship --verbose`` + Rebalance prayer needs of units in the fort. Print names of affected units. diff --git a/fix/stuck-worship.lua b/fix/stuck-worship.lua index 1531af139a..0222dd6fe0 100644 --- a/fix/stuck-worship.lua +++ b/fix/stuck-worship.lua @@ -1,4 +1,9 @@ -debug_print = false +local argparse = require('argparse') + +local verbose = false +argparse.processArgsGetopt({...}, { + {'v', 'verbose', handler=function() verbose = true end}, +}) local function for_pray_need(needs, fn) for idx, need in ipairs(needs) do @@ -81,15 +86,26 @@ local function get_prayer_targets(unit) end end +local function unit_name(unit) + return dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) +end + +local count = 0 for _,unit in ipairs(dfhack.units.getCitizens(false, true)) do local prayer_targets = get_prayer_targets(unit) if not unit.status.current_soul or not prayer_targets then goto next_unit end local needs = unit.status.current_soul.personality.needs - if shuffle_prayer_needs(needs, prayer_targets) and debug_print then - print('rebalanced prayer needs for ' .. - dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit)))) + if shuffle_prayer_needs(needs, prayer_targets) then + count = count + 1 + if verbose then + print('Shuffled prayer target for '..unit_name(unit)) + end end ::next_unit:: end + +if verbose or count > 0 then + print(('Rebalanced prayer needs for %d units.'):format(count)) +end From cbffc93a12786768dcf010f873ed391709b415a8 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Sun, 29 Sep 2024 21:06:00 -0700 Subject: [PATCH 131/811] Add quiet option * Update stuck-worship.rst * Update stuck-worship.lua * Update registry.lua --- docs/fix/stuck-worship.rst | 25 ++++++++++++++++++++++--- fix/stuck-worship.lua | 5 +++-- internal/control-panel/registry.lua | 2 +- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/docs/fix/stuck-worship.rst b/docs/fix/stuck-worship.rst index c4bccc0554..2854be122b 100644 --- a/docs/fix/stuck-worship.rst +++ b/docs/fix/stuck-worship.rst @@ -26,7 +26,26 @@ another task. Usage ----- +:: + + fix/stuck-worship [] + +Reshuffle prayer needs of units in the fort. + +Examples +-------- + ``fix/stuck-worship`` - Rebalance prayer needs of units in the fort. -``fix/stuck-worship -v``, ``fix/stuck-worship --verbose`` - Rebalance prayer needs of units in the fort. Print names of affected units. + Rebalance prayer needs and print the total number of affected units. +``fix/stuck-worship -v`` + Same as above, but also print the names of all affected units. + +Options +------- + +``-v``, ``--verbose`` + Print the names of all affected units. +``-q``, ``--quiet`` + Don't print the number of affected units if it's zero. Intended for + automatic use. + diff --git a/fix/stuck-worship.lua b/fix/stuck-worship.lua index 0222dd6fe0..2d48809141 100644 --- a/fix/stuck-worship.lua +++ b/fix/stuck-worship.lua @@ -1,8 +1,9 @@ local argparse = require('argparse') -local verbose = false +local verbose, quiet = false, false argparse.processArgsGetopt({...}, { {'v', 'verbose', handler=function() verbose = true end}, + {'q', 'quiet', handler=function() quiet = true end}, }) local function for_pray_need(needs, fn) @@ -106,6 +107,6 @@ for _,unit in ipairs(dfhack.units.getCitizens(false, true)) do ::next_unit:: end -if verbose or count > 0 then +if not quiet or count > 0 then print(('Rebalanced prayer needs for %d units.'):format(count)) end diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 4e0bdd2e67..7dca4d2b01 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -90,7 +90,7 @@ COMMANDS_BY_IDX = { desc='Fix activity references on stuck instruments to make them usable again.', params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, {command='fix/stuck-worship', group='bugfix', mode='repeat', default=true, - params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-worship', ']'}}, + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-worship', '-q', ']'}}, {command='fix/noexert-exhaustion', group='bugfix', mode='repeat', default=true, params={'--time', '439', '--timeUnits', 'ticks', '--command', '[', 'fix/noexert-exhaustion', ']'}}, {command='flask-contents', help_command='tweak', group='bugfix', mode='tweak', default=true, From 5b43f4a01b53de91cdc0b8b32f7e9de51c667a6c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 30 Sep 2024 04:07:29 +0000 Subject: [PATCH 132/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/fix/stuck-worship.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/fix/stuck-worship.rst b/docs/fix/stuck-worship.rst index 2854be122b..04c4129364 100644 --- a/docs/fix/stuck-worship.rst +++ b/docs/fix/stuck-worship.rst @@ -48,4 +48,3 @@ Options ``-q``, ``--quiet`` Don't print the number of affected units if it's zero. Intended for automatic use. - From 8d265ed42cd674d9074c4cb9806a71341399f452 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Mon, 30 Sep 2024 15:16:19 -0700 Subject: [PATCH 133/811] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 0a14d6a6e6..7507a87ba1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,7 +36,7 @@ Template for new versions: ## Misc Improvements - `control-panel`: Add realistic-melting tweak to control-panel registry - `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles -- `fix/stuck-worship`: reduced console output by default. ``--verbose`` option to print all affected units. +- `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. ## Removed From c6fa952d198c87e97ce79467376abdf7f3f896db Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 2 Oct 2024 06:43:01 -0700 Subject: [PATCH 134/811] convert numeric strings to numbers for numeric preferences --- changelog.txt | 1 + internal/control-panel/common.lua | 3 +++ 2 files changed, 4 insertions(+) diff --git a/changelog.txt b/changelog.txt index 7507a87ba1..b58d36042f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -32,6 +32,7 @@ Template for new versions: ## Fixes - `gui/quickfort`: only print a help blueprint's text once even if the repeat setting is enabled +- `control-panel`: fix setting numeric preferences from the commandline ## Misc Improvements - `control-panel`: Add realistic-melting tweak to control-panel registry diff --git a/internal/control-panel/common.lua b/internal/control-panel/common.lua index 21472c45c1..df4e9f6f7a 100644 --- a/internal/control-panel/common.lua +++ b/internal/control-panel/common.lua @@ -180,6 +180,9 @@ function set_preference(data, in_value) if expected_type == 'boolean' and type(value) ~= 'boolean' then value = argparse.boolean(value) end + if expected_type == "number" then + value = tonumber(value) + end local actual_type = type(value) if actual_type ~= expected_type then qerror(('"%s" has an unexpected value type: got: %s; expected: %s'):format( From 1c2b1a03fe75889a84d2dcc2dd15d4031974df11 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 2 Oct 2024 06:44:42 -0700 Subject: [PATCH 135/811] use original type if not numeric --- internal/control-panel/common.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/control-panel/common.lua b/internal/control-panel/common.lua index df4e9f6f7a..922f053644 100644 --- a/internal/control-panel/common.lua +++ b/internal/control-panel/common.lua @@ -181,7 +181,7 @@ function set_preference(data, in_value) value = argparse.boolean(value) end if expected_type == "number" then - value = tonumber(value) + value = tonumber(value) or value end local actual_type = type(value) if actual_type ~= expected_type then From a84b80fe9a4752ab2dbf7ecd5dd34676bf2e3687 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Oct 2024 05:49:43 -0700 Subject: [PATCH 136/811] support stringification of language_name fields and an option to turn it off in case of need to browse garbage --- changelog.txt | 1 + docs/gui/gm-editor.rst | 41 ++++++++++++-------- gui/gm-editor.lua | 86 ++++++++++++++++++++++-------------------- 3 files changed, 72 insertions(+), 56 deletions(-) diff --git a/changelog.txt b/changelog.txt index b58d36042f..c2fa9f4423 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,6 +37,7 @@ Template for new versions: ## Misc Improvements - `control-panel`: Add realistic-melting tweak to control-panel registry - `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles +- `gui/gm-editor`: automatic display of semantic values for language_name fields - `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. ## Removed diff --git a/docs/gui/gm-editor.rst b/docs/gui/gm-editor.rst index 084b367ecf..2b82cefd17 100644 --- a/docs/gui/gm-editor.rst +++ b/docs/gui/gm-editor.rst @@ -16,9 +16,10 @@ Hold down :kbd:`Shift` and right click to exit, even if you are inspecting a substructure, no matter how deep. If you just want to browse without fear of accidentally changing anything, hit -:kbd:`Ctrl`:kbd:`D` to toggle read-only mode. If you want `gui/gm-editor` to -automatically pick up changes to game data in realtime, hit :kbd:`Alt`:kbd:`A` -to switch to auto update mode. +:kbd:`Ctrl`:kbd:`D` to toggle read-only mode. + +If you want `gui/gm-editor` to automatically pick up changes to game data in +realtime, hit :kbd:`Alt`:kbd:`A` to switch to auto update mode. .. warning:: @@ -31,15 +32,13 @@ to switch to auto update mode. Usage ----- -``gui/gm-editor [-f]`` - Open the editor on whatever is selected or viewed (e.g. unit/item/building/ - engraving/etc.) -``gui/gm-editor [-f] `` - Evaluate a lua expression and opens the editor on its results. Field - prefixes of ``df.global`` can be omitted. -``gui/gm-editor [-f] dialog`` - Show an in-game dialog to input the lua expression to evaluate. Works the - same as the version above. +:: + + gui/gm-editor [] [] + gui/gm-editor [] dialog + +When specifying a lua expression, field prefixes of ``df.global`` can be +omitted. Examples -------- @@ -48,15 +47,22 @@ Examples Opens the editor on the selected unit/item/job/workorder/stockpile etc. ``gui/gm-editor world.items.all`` Opens the editor on the items list. +``gui/gm-editor df.unit.find(12345)`` + Opens the editor on the unit with id 12345. +``gui/gm-editor reqscript('gui/quickfort').view`` + Opens the editor on a running instance of `gui/quickfort`. Useful for + debugging GUI tool state during development. ``gui/gm-editor --freeze scr`` Opens the editor on the current DF viewscreen data (bypassing any DFHack - layers) and prevents the underlying viewscreen from getting updates while - you have the editor open. + tools that may be open) and prevents the underlying viewscreen from getting + updates while you are inspecting the data. +``gui/gm-editor dialog`` + Show an in-game dialog to input the lua expression to evaluate. Options ------- -``-f``, ``--freeze`` +``-f``, ``--freeze``, ``--safe-mode`` Freeze the underlying viewscreen so that it does not receive any updates. This allows you to be sure that whatever you are inspecting or modifying will not be read or changed by the game until you are done with it. Note @@ -65,6 +71,11 @@ Options `gui/gm-editor` as usual when the game is frozen. The black background will disappear when the last `gui/gm-editor` window that was opened with the ``--freeze`` option is dismissed. +``--no-stringification`` + Don't attempt to provide helpful string representations of potentially + unsafe fields like language_name when browsing the data structures. Specify + this option when you know you will be browsing garbage data that could lead + to crashes if accessed for stringification. Screenshot ---------- diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 9ec7cdd2ce..ac5126617a 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -1,12 +1,13 @@ -- Interface powered memory object editor. --@module=true -local gui = require 'gui' -local json = require 'json' -local dialog = require 'gui.dialogs' -local widgets = require 'gui.widgets' -local guiScript = require 'gui.script' -local utils = require 'utils' +local argparse = require('argparse') +local gui = require('gui') +local json = require('json') +local dialog = require('gui.dialogs') +local widgets = require('gui.widgets') +local guiScript = require('gui.script') +local utils = require('utils') config = config or json.open('dfhack-config/gm-editor.json') @@ -124,7 +125,8 @@ GmEditorUi.ATTRS{ frame_inset=0, resizable=true, resize_min=RESIZE_MIN, - read_only=(config.data.read_only or false) + read_only=(config.data.read_only or false), + helpers=true, } function burning_red(input) -- todo does not work! bug angavrilov that so that he would add this, very important!! @@ -621,27 +623,31 @@ function GmEditorUi:onInput(keys) end end -function getStringValue(trg,field) +function GmEditorUi:getStringValue(trg, field) local obj=trg.target local text=tostring(obj[field]) pcall(function() - if obj._field ~= nil then - local f = obj:_field(field) - if df.coord:is_instance(f) then - text=('(%d, %d, %d) '):format(f.x, f.y, f.z) .. text - elseif df.coord2d:is_instance(f) then - text=('(%d, %d) '):format(f.x, f.y) .. text - end - local enum=f._type - if enum._kind=="enum-type" then - text=text.." ("..tostring(enum[obj[field]])..")" - end - local ref_target=f.ref_target - if ref_target then - text=text.. " (ref-target: "..getmetatable(ref_target)..")" + if obj._field ~= nil then + local f = obj:_field(field) + if self.helpers then + if df.coord:is_instance(f) then + text=('(%d, %d, %d) %s'):format(f.x, f.y, f.z, text) + elseif df.coord2d:is_instance(f) then + text=('(%d, %d) %s'):format(f.x, f.y, text) + elseif df.language_name:is_instance(f) then + text=('%s (%s) %s'):format(dfhack.TranslateName(f, false), dfhack.TranslateName(f, true), text) + end + end + local enum=f._type + if enum._kind=="enum-type" then + text=text.." ("..tostring(enum[obj[field]])..")" + end + local ref_target=f.ref_target + if ref_target then + text=text.. " (ref-target: "..getmetatable(ref_target)..")" + end end - end end) return text end @@ -684,7 +690,7 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) self.subviews.lbl_current_item:itemById('name').text=tostring(trg.target) local t={} for k,v in pairs(trg.keys) do - table.insert(t,{text={{text=string.format("%-"..trg.kw.."s",tostring(v))},{gap=2,text=getStringValue(trg,v)}}}) + table.insert(t,{text={{text=string.format("%-"..trg.kw.."s",tostring(v))},{gap=2,text=self:getStringValue(trg,v)}}}) end local last_selected, last_top if preserve_pos then @@ -824,7 +830,7 @@ function GmScreen:init(args) end end end - self:addviews{GmEditorUi{target=target}} + self:addviews{GmEditorUi{target=target, helpers=args.helpers}} views[self] = true end @@ -838,28 +844,26 @@ function GmScreen:onDismiss() end local function get_editor(args) - local freeze = false - if args[1] == '-f' or args[1] == '--freeze' then - freeze = true - table.remove(args, 1) - end - if #args~=0 then - if args[1]=="dialog" then - dialog.showInputPrompt("Gm Editor", "Object to edit:", COLOR_GRAY, + local freeze, helpers = false, true + local positionals = argparse.processArgsGetopt(args, { + {'f', 'freeze', 'safe-mode', handler=function() freeze = true end}, + {nil, 'no-stringification', handler=function() helpers = false end}, + }) + if #positionals == 0 then + GmScreen{freeze=freeze, helpers=helpers, target=getTargetFromScreens()}:show() + else + if positionals[1]=="dialog" then + dialog.showInputPrompt("GM Editor", "Object to edit:", COLOR_GRAY, "", function(entry) - GmScreen{freeze=freeze, target=eval(entry)}:show() + GmScreen{freeze=freeze, helpers=helpers, target=eval(entry)}:show() end) - elseif args[1]=="free" then - GmScreen{freeze=freeze, target=df.reinterpret_cast(df[args[2]],args[3])}:show() - elseif args[1]=="scr" then + elseif positionals[1]=="scr" then -- this will not work for more complicated expressions, like scr.fieldname, but -- it should capture the most common case - GmScreen{freeze=freeze, target=dfhack.gui.getDFViewscreen(true)}:show() + GmScreen{freeze=freeze, helpers=helpers, target=dfhack.gui.getDFViewscreen(true)}:show() else - GmScreen{freeze=freeze, target=eval(args[1])}:show() + GmScreen{freeze=freeze, helpers=helpers, target=eval(positionals[1])}:show() end - else - GmScreen{freeze=freeze, target=getTargetFromScreens()}:show() end end From bc6c29882250f9e62a7d29e49ffa82864a32aee8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Oct 2024 11:07:23 -0700 Subject: [PATCH 137/811] don't stringify members of unions --- docs/gui/gm-editor.rst | 3 ++- gui/gm-editor.lua | 40 +++++++++++++++++++++------------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/docs/gui/gm-editor.rst b/docs/gui/gm-editor.rst index 2b82cefd17..50ff88e142 100644 --- a/docs/gui/gm-editor.rst +++ b/docs/gui/gm-editor.rst @@ -75,7 +75,8 @@ Options Don't attempt to provide helpful string representations of potentially unsafe fields like language_name when browsing the data structures. Specify this option when you know you will be browsing garbage data that could lead - to crashes if accessed for stringification. + to crashes if accessed for stringification. Note that fields in union data + structures are never stringified. Screenshot ---------- diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index ac5126617a..eeaa0d1601 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -624,30 +624,32 @@ function GmEditorUi:onInput(keys) end function GmEditorUi:getStringValue(trg, field) - local obj=trg.target + local obj = trg.target + local is_union = obj._type._union local text=tostring(obj[field]) pcall(function() - if obj._field ~= nil then - local f = obj:_field(field) - if self.helpers then - if df.coord:is_instance(f) then - text=('(%d, %d, %d) %s'):format(f.x, f.y, f.z, text) - elseif df.coord2d:is_instance(f) then - text=('(%d, %d) %s'):format(f.x, f.y, text) - elseif df.language_name:is_instance(f) then - text=('%s (%s) %s'):format(dfhack.TranslateName(f, false), dfhack.TranslateName(f, true), text) - end - end - local enum=f._type - if enum._kind=="enum-type" then - text=text.." ("..tostring(enum[obj[field]])..")" - end - local ref_target=f.ref_target - if ref_target then - text=text.. " (ref-target: "..getmetatable(ref_target)..")" + if obj._field == nil then return end + local f = obj:_field(field) + if self.helpers and not is_union then + if df.coord:is_instance(f) then + text=('(%d, %d, %d) %s'):format(f.x, f.y, f.z, text) + elseif df.coord2d:is_instance(f) then + text=('(%d, %d) %s'):format(f.x, f.y, text) + elseif df.language_name:is_instance(f) then + text=('%s (%s) %s'):format(dfhack.TranslateName(f, false), dfhack.TranslateName(f, true), text) end end + local enum = f._type + if enum._kind=="enum-type" then + text=text.." ("..tostring(enum[obj[field]])..")" + end + -- this will throw for types that have no ref target; pcall will catch it, but make sure this bit stays + -- at the end of the pcall function body + local ref_target=f.ref_target + if ref_target then + text=text.. " (ref-target: "..getmetatable(ref_target)..")" + end end) return text end From 8d7fd409179dbb4f5f4e47628d4294f8ecca8d1e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Oct 2024 11:24:32 -0700 Subject: [PATCH 138/811] add warning when viewing a union structure --- gui/gm-editor.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index eeaa0d1601..7fc8b7278c 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -185,7 +185,7 @@ function GmEditorUi:init(args) local mainPage=widgets.Panel{ subviews={ mainList, - widgets.Label{text={{text="",id="name"},{gap=1,text="Help",key=keybindings.help.key,key_sep = '()'}}, view_id = 'lbl_current_item',frame = {l=1,t=1,yalign=0}}, + widgets.Label{text={{text="",id="name"},{text="",pen=COLOR_RED,id="union"},{gap=1,text="Help",key=keybindings.help.key,key_sep = '()'}}, view_id = 'lbl_current_item',frame = {l=1,t=1,yalign=0}}, widgets.EditField{frame={l=1,t=2,h=1},label_text="Search",key=keybindings.start_filter.key,key_sep='(): ',on_change=self:callback('text_input'),view_id="filter_input"}} ,view_id='page_main'} @@ -625,13 +625,12 @@ end function GmEditorUi:getStringValue(trg, field) local obj = trg.target - local is_union = obj._type._union local text=tostring(obj[field]) pcall(function() if obj._field == nil then return end local f = obj:_field(field) - if self.helpers and not is_union then + if self.helpers and not obj._type._union then if df.coord:is_instance(f) then text=('(%d, %d, %d) %s'):format(f.x, f.y, f.z, text) elseif df.coord2d:is_instance(f) then @@ -689,6 +688,7 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) end end end + self.subviews.lbl_current_item:itemById('union').text = trg.target._type._union and " [union structure]" or "" self.subviews.lbl_current_item:itemById('name').text=tostring(trg.target) local t={} for k,v in pairs(trg.keys) do From 88493bf821bbd6adfb8a5c24617d9fb24900b642 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Oct 2024 14:39:34 -0700 Subject: [PATCH 139/811] red -> cyan since it's not an error --- gui/gm-editor.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 7fc8b7278c..e341ee73ef 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -185,7 +185,7 @@ function GmEditorUi:init(args) local mainPage=widgets.Panel{ subviews={ mainList, - widgets.Label{text={{text="",id="name"},{text="",pen=COLOR_RED,id="union"},{gap=1,text="Help",key=keybindings.help.key,key_sep = '()'}}, view_id = 'lbl_current_item',frame = {l=1,t=1,yalign=0}}, + widgets.Label{text={{text="",id="name"},{text="",pen=COLOR_CYAN,id="union"},{gap=1,text="Help",key=keybindings.help.key,key_sep = '()'}}, view_id = 'lbl_current_item',frame = {l=1,t=1,yalign=0}}, widgets.EditField{frame={l=1,t=2,h=1},label_text="Search",key=keybindings.start_filter.key,key_sep='(): ',on_change=self:callback('text_input'),view_id="filter_input"}} ,view_id='page_main'} From ec52b429f769ac075ef78a4b4d999fefed1fa49a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Oct 2024 18:31:42 -0700 Subject: [PATCH 140/811] initial implementation of fix/wildlife --- changelog.txt | 1 + docs/fix/wildlife.rst | 63 ++++++++++++ fix/wildlife.lua | 149 ++++++++++++++++++++++++++++ internal/control-panel/registry.lua | 2 + 4 files changed, 215 insertions(+) create mode 100644 docs/fix/wildlife.rst create mode 100644 fix/wildlife.lua diff --git a/changelog.txt b/changelog.txt index b58d36042f..f635ae76c4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed. ## New Features diff --git a/docs/fix/wildlife.rst b/docs/fix/wildlife.rst new file mode 100644 index 0000000000..b92b235796 --- /dev/null +++ b/docs/fix/wildlife.rst @@ -0,0 +1,63 @@ +fix/wildlife +============ + +.. dfhack-tool:: + :summary: Moves stuck wildlife off the map so new waves can enter. + :tags: fort bugfix animals + +This tool identifies wildlife that is trying to leave the map but has gotten +stuck. The stuck creatures will be moved off the map so that new waves of +wildlife can enter. When removing stuck wildlife, their regional population +counters are correctly incremented, just as if they had successfully left the +map on their own. + +Dwarf Fortress manages wildlife in "waves". A small group of creatures of a +species that has population associated with a local region enters the map, +wanders around for a while (or aggressively attacks you if it is an agitated +group), and then leaves the map. Any members of the group that successfully +leave the map will get added back to the regional population. + +The trouble, though, is that the group sometimes gets stuck when attempting to +leave. A new wave cannot enter until the previous group has been destroyed or +has left the map, so wildlife activity effectively completely halts. This is DF +:bug:`12921`. + +You can run this script without parameters to immediately remove stuck +wildlife, or you can enable it in the `gui/control-panel` on the Bug Fixes tab +to monitor and manage wildlife in the background. When enabled from the control +panel, it will monitor for stuck wildlife and remove wildlife after it has been +stuck for 7 days. + +Unlike most bugfixes, this one is not enabled by default since some players +like to keep wildlife around for creative purposes (e.g. for intentionally +stalling wildlife waves or for controlled startling of friendly necromancers). + +Usage +----- +:: + + fix/wildlife [] + +Examples +-------- + +``fix/wildlife`` + Remove any wildlife that is currently trying to leave the map but has not + yet succeeded. +``fix/wildlife --week`` + Remove wildlife that has been stuck for at least a week. The command must + be run periodically with this option so it can discover newly stuck + wildlife and remove wildlife when timeouts expire. + +Options +------- + +``-n``, ``--dry-run`` + Print out which creatures are stuck but take no action. +``-w``, ``--week`` + Discover newly stuck units and associate the current in-game time with + them. Units that were discovered on a previous invocation where this + parameter was specified will be removed if that time was at least a week + ago. +``-q``, ``--quiet`` + Don't print the number of affected units if no units were affected. diff --git a/fix/wildlife.lua b/fix/wildlife.lua new file mode 100644 index 0000000000..7ad024f65d --- /dev/null +++ b/fix/wildlife.lua @@ -0,0 +1,149 @@ +--@module = true + +local argparse = require('argparse') +local exterminate = reqscript('exterminate') + +local GLOBAL_KEY = 'fix/wildlife' + +DEBUG = DEBUG or false + +stuck_creatures = stuck_creatures or {} + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if (sc == SC_MAP_UNLOADED or sc == SC_MAP_LOADED) and + dfhack.world.isFortressMode() + then + stuck_creatures = {} + end +end + +local function print_summary(opts, unstuck) + if not next(unstuck) then + if not opts.quiet then + print('No stuck wildlife found') + return + end + end + local prefix = opts.week and (GLOBAL_KEY .. ': ') or '' + local msg_txt = opts.dry_run and '' or 'no longer ' + for _,entry in pairs(unstuck) do + if entry.count == 1 then + print(('%s%d %s is %sblocking new waves of wildlife'):format( + prefix, + entry.count, + entry.known and dfhack.units.getRaceReadableNameById(entry.race) or 'hidden creature', + msg_txt)) + else + print(('%s%d %s are %sblocking new waves of wildlife'):format( + prefix, + entry.count, + entry.known and dfhack.units.getRaceNamePluralById(entry.race) or 'hidden creatures', + msg_txt)) + end + end +end + +local function refund_population(entry) + local epop = entry.pop + for _,population in ipairs(df.global.world.populations) do + local wpop = population.population + if population.quantity < 10000001 and + wpop.region_x == epop.region_x and + wpop.region_y == epop.region_y and + wpop.feature_idx == epop.feature_idx and + wpop.cave_id == epop.cave_id and + wpop.site_id == epop.site_id and + wpop.population_idx == epop.population_idx + then + population.quantity = math.min(population.quantity + entry.count, population.quantity_max) + break + end + end +end + +local TICKS_PER_DAY = 1200 +local TICKS_PER_WEEK = TICKS_PER_DAY * 7 +local TICKS_PER_MONTH = 28 * TICKS_PER_DAY +local TICKS_PER_SEASON = 3 * TICKS_PER_MONTH +local TICKS_PER_YEAR = 4 * TICKS_PER_SEASON + +local WEEK_BEFORE_EOY_TICKS = TICKS_PER_YEAR - TICKS_PER_WEEK + +-- update stuck_creatures records and check timeout +-- we only enter this function if the unit's leave_countdown has already expired +-- returns true if the unit has timed out +local function check_timeout(opts, unit, week_ago_ticks) + if not opts.week then return true end + if not stuck_creatures[unit.id] then + stuck_creatures[unit.id] = df.global.cur_year_tick + return false + end + local timestamp = stuck_creatures[unit.id] + return timestamp < week_ago_ticks or + (timestamp > df.global.cur_year_tick and timestamp > WEEK_BEFORE_EOY_TICKS) +end + +local function to_key(pop) + return ('%d:%d:%d:%d:%d:%d'):format( + pop.region_x, pop.region_y, pop.feature_idx, pop.cave_id, pop.site_id, pop.population_idx) +end + +local function unstick_surface_wildlife(opts) + local unstuck = {} + local week_ago_ticks = math.max(0, df.global.cur_year_tick - TICKS_PER_WEEK) + for _,unit in ipairs(df.global.world.units.active) do + if dfhack.units.isDead(unit) or + not dfhack.units.isActive(unit) or + not dfhack.units.isWildlife(unit) or + not unit.flags2.roaming_wilderness_population_source or + unit.animal.leave_countdown > 0 + then + goto skip + end + if not check_timeout(opts, unit, week_ago_ticks) then + goto skip + end + local pop = unit.animal.population + local unstuck_entry = ensure_key(unstuck, to_key(pop), {race=unit.race, pop=pop, known=false, count=0}) + unstuck_entry.known = unstuck_entry.known or not dfhack.units.isHidden(unit) + unstuck_entry.count = unstuck_entry.count + 1 + if not opts.dry_run then + stuck_creatures[unit.id] = nil + exterminate.killUnit(unit, exterminate.killMethod.DISINTEGRATE) + end + ::skip:: + end + for _,entry in pairs(unstuck) do + refund_population(entry) + end + print_summary(opts, unstuck) +end + +if dfhack_flags.module then + return +end + +if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('needs a loaded fortress map to work') +end + +local opts = { + dry_run=false, + help=false, + quiet=false, + week=false, +} + +local positionals = argparse.processArgsGetopt({...}, { + {'h', 'help', handler = function() opts.help = true end}, + {'n', 'dry-run', handler = function() opts.dry_run = true end}, + {'w', 'week', handler = function() opts.week = true end}, + {'q', 'quiet', handler = function() opts.quiet = true end}, +}) + +if positionals[1] == 'help' or opts.help then + print(dfhack.script_help()) + return +end + +unstick_surface_wildlife(opts) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 7dca4d2b01..aa35e5ae5e 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -93,6 +93,8 @@ COMMANDS_BY_IDX = { params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-worship', '-q', ']'}}, {command='fix/noexert-exhaustion', group='bugfix', mode='repeat', default=true, params={'--time', '439', '--timeUnits', 'ticks', '--command', '[', 'fix/noexert-exhaustion', ']'}}, + {command='fix/wildlife', group='bugfix', mode='repeat', + params={'--time', '2', '--timeUnits', 'days', '--command', '[', 'fix/wildlife', '-wq', ']'}}, {command='flask-contents', help_command='tweak', group='bugfix', mode='tweak', default=true, desc='Displays flask contents in the item name, similar to barrels and bins.'}, {command='named-codices', help_command='tweak', group='bugfix', mode='tweak', default=true, From 353871bbb08bb69926b606150a3cfee07d057899 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Oct 2024 19:47:34 -0700 Subject: [PATCH 141/811] implement fix/wildlife ignore --- docs/fix/wildlife.rst | 7 +++++++ fix/wildlife.lua | 43 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 43 insertions(+), 7 deletions(-) diff --git a/docs/fix/wildlife.rst b/docs/fix/wildlife.rst index b92b235796..648f1f4dde 100644 --- a/docs/fix/wildlife.rst +++ b/docs/fix/wildlife.rst @@ -31,12 +31,15 @@ stuck for 7 days. Unlike most bugfixes, this one is not enabled by default since some players like to keep wildlife around for creative purposes (e.g. for intentionally stalling wildlife waves or for controlled startling of friendly necromancers). +These players can selectively ignore the wildlife they want to keep captive +before they enable `fix/wildlife`. Usage ----- :: fix/wildlife [] + fix/wildlife ignore [unit ID] Examples -------- @@ -48,6 +51,10 @@ Examples Remove wildlife that has been stuck for at least a week. The command must be run periodically with this option so it can discover newly stuck wildlife and remove wildlife when timeouts expire. +``fix/wildlife ignore`` + Disconnect the selected unit from its wildlife population so it doesn't + block new wildlife from entering the map, but keep the unit on the map. + This unit will not be touched by future invocations of this tool. Options ------- diff --git a/fix/wildlife.lua b/fix/wildlife.lua index 7ad024f65d..cbd5e13b20 100644 --- a/fix/wildlife.lua +++ b/fix/wildlife.lua @@ -61,6 +61,13 @@ local function refund_population(entry) end end +-- refund unit to population and ensure it doesn't get picked up by unstick_surface_wildlife in the future +local function detach_unit(unit) + unit.flags2.roaming_wilderness_population_source = false + unit.flags2.roaming_wilderness_population_source_not_a_map_feature = false + refund_population{race=unit.race, pop=unit.animal.population, known=true, count=1} +end + local TICKS_PER_DAY = 1200 local TICKS_PER_WEEK = TICKS_PER_DAY * 7 local TICKS_PER_MONTH = 28 * TICKS_PER_DAY @@ -88,16 +95,18 @@ local function to_key(pop) pop.region_x, pop.region_y, pop.feature_idx, pop.cave_id, pop.site_id, pop.population_idx) end +local function is_active_wildlife(unit) + return not dfhack.units.isDead(unit) and + dfhack.units.isActive(unit) and + dfhack.units.isWildlife(unit) and + unit.flags2.roaming_wilderness_population_source +end + local function unstick_surface_wildlife(opts) local unstuck = {} local week_ago_ticks = math.max(0, df.global.cur_year_tick - TICKS_PER_WEEK) for _,unit in ipairs(df.global.world.units.active) do - if dfhack.units.isDead(unit) or - not dfhack.units.isActive(unit) or - not dfhack.units.isWildlife(unit) or - not unit.flags2.roaming_wilderness_population_source or - unit.animal.leave_countdown > 0 - then + if not is_active_wildlife(unit) or unit.animal.leave_countdown > 0 then goto skip end if not check_timeout(opts, unit, week_ago_ticks) then @@ -146,4 +155,24 @@ if positionals[1] == 'help' or opts.help then return end -unstick_surface_wildlife(opts) +if positionals[1] == 'ignore' then + local unit + local unit_id = positionals[2] and argparse.nonnegativeInt(positionals[2], 'unit_id') + if unit_id then + unit = df.unit.find(unit_id) + else + unit = dfhack.gui.getSelectedUnit(true) + end + if not unit then + qerror('please select a unit or pass a unit ID on the commandline') + end + if not is_active_wildlife(unit) then + qerror('selected unit is not blocking new waves of wildlife; nothing to do') + end + detach_unit(unit) + if not opts.quiet then + print(('%s will now be ignored by fix/wildlife'):format(dfhack.units.getReadableName(unit))) + end +else + unstick_surface_wildlife(opts) +end From 54267e73410358834c534cc6335463b2186129cb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 07:05:19 -0700 Subject: [PATCH 142/811] add warning about union data structures --- docs/gui/gm-editor.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/docs/gui/gm-editor.rst b/docs/gui/gm-editor.rst index 50ff88e142..e2a34a488b 100644 --- a/docs/gui/gm-editor.rst +++ b/docs/gui/gm-editor.rst @@ -29,6 +29,16 @@ realtime, hit :kbd:`Alt`:kbd:`A` to switch to auto update mode. your game before poking around in `gui/gm-editor`, especially if you are examining data while the game is unpaused. +.. warning:: + + Union data structures contain fields that occupy the same memory space. + When you see the ``[union structure]`` badge at the top of the screen, be + aware that only one of the fields in the structure is likely to make sense. + The "correct" field is usually indicated by some context in the parent + structure. If there are any pointers to substructures in the union, + inspecting the pointer when it is not the "correct" field may crash the + game. + Usage ----- From 4564717d26e937c4fe92a7941248c1d333a8dc58 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 11:08:22 -0700 Subject: [PATCH 143/811] don't attempt to get _union field of non-userdata --- gui/gm-editor.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index e341ee73ef..6d37f96c5f 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -688,7 +688,7 @@ function GmEditorUi:updateTarget(preserve_pos,reindex) end end end - self.subviews.lbl_current_item:itemById('union').text = trg.target._type._union and " [union structure]" or "" + self.subviews.lbl_current_item:itemById('union').text = type(trg.target) == 'userdata' and trg.target._type._union and " [union structure]" or "" self.subviews.lbl_current_item:itemById('name').text=tostring(trg.target) local t={} for k,v in pairs(trg.keys) do From 9c996594bdf3b15a8a93a19bbfbeca20b87127fa Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 13:44:08 -0700 Subject: [PATCH 144/811] use new squads focus strings --- gui/civ-alert.lua | 11 +++++++---- internal/confirm/specs.lua | 2 +- uniform-unstick.lua | 2 +- 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 29ee4cc561..37bec014de 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -118,14 +118,17 @@ CivalertOverlay.ATTRS{ frame={w=20, h=5}, } +local function is_squads_panel_open() + return dfhack.gui.matchFocusString('dwarfmode/Squads/Default', + dfhack.gui.getDFViewscreen(true)) +end + local function should_show_alert_button() - return can_clear_alarm() or - (df.global.game.main_interface.squads.open and can_sound_alarm()) + return can_clear_alarm() or (is_squads_panel_open() and can_sound_alarm()) end local function should_show_configure_button() - return df.global.game.main_interface.squads.open - and not can_sound_alarm() and not can_clear_alarm() + return is_squads_panel_open() and not can_sound_alarm() and not can_clear_alarm() end local function launch_config() diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index cbb33cd90e..a6e0301eb4 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -330,7 +330,7 @@ ConfirmSpec{ -- sticks out the left side so it can move with the panel -- when the screen is resized too narrow intercept_frame={r=32, t=19, w=101, b=3}, - context='dwarfmode/SquadEquipment/Customizing/Default', + context='dwarfmode/Squads/Equipment/Customizing/Default', predicate=function(keys, mouse_offset) if keys._MOUSE_R then return uniform_has_changes() diff --git a/uniform-unstick.lua b/uniform-unstick.lua index af86aa8006..04846e7675 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -332,7 +332,7 @@ EquipOverlay.ATTRS{ desc='Adds a link to the equip screen to fix equipment conflicts.', default_pos={x=7,y=21}, default_enabled=true, - viewscreens='dwarfmode/SquadEquipment/Default', + viewscreens='dwarfmode/Squads/Equipment/Default', frame={w=MIN_WIDTH, h=1}, } From bf0ec1b8593f32011971bec6241e877955b8c799 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 14:16:36 -0700 Subject: [PATCH 145/811] redo layout of conflict report dialog --- uniform-unstick.lua | 49 ++++++++++++++++++++++++++++----------------- 1 file changed, 31 insertions(+), 18 deletions(-) diff --git a/uniform-unstick.lua b/uniform-unstick.lua index af86aa8006..b404843328 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -283,34 +283,47 @@ end ReportWindow = defclass(ReportWindow, widgets.Window) ReportWindow.ATTRS { frame_title='Equipment conflict report', - frame={w=100, h=45}, - resizable=true, -- if resizing makes sense for your dialog - resize_min={w=50, h=20}, -- try to allow users to shrink your windows - autoarrange_subviews=1, - autoarrange_gap=1, + frame={w=100, h=35}, + resizable=true, + resize_min={w=60, h=20}, report=DEFAULT_NIL, } function ReportWindow:init() self:addviews{ - widgets.HotkeyLabel{ - frame={t=0, l=0, r=0}, - label='Try to resolve conflicts', - key='CUSTOM_CTRL_T', - auto_width=true, - on_activate=function() - dfhack.run_script('uniform-unstick', '--all', '--drop', '--free') - self.parent_view:dismiss() - end, + widgets.Label{ + frame={t=0, l=0}, + text_pen=COLOR_YELLOW, + text='Equipment conflict report:', + }, + widgets.Panel{ + frame={t=2, b=7}, + subviews={ + widgets.WrappedLabel{ + frame={t=0}, + text_to_wrap=self.report, + }, + }, }, widgets.WrappedLabel{ - frame={t=2, l=0, r=0}, + frame={b=4, h=2, l=0}, text_pen=COLOR_LIGHTRED, text_to_wrap='After resolving conflicts, be sure to click the "Update equipment" button to reassign new equipment!', + auto_height=false, }, - widgets.WrappedLabel{ - frame={t=4, l=0, r=0}, - text_to_wrap=self.report, + widgets.Panel{ + frame={b=0, w=34, h=3}, + frame_style=gui.FRAME_THIN, + subviews={ + widgets.HotkeyLabel{ + label='Try to resolve conflicts', + key='CUSTOM_CTRL_T', + on_activate=function() + dfhack.run_script('uniform-unstick', '--all', '--drop', '--free') + self.parent_view:dismiss() + end, + }, + }, }, } end From 5e7a7a01470e74319852dd876ce5a7cb055cee43 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 15:59:10 -0700 Subject: [PATCH 146/811] prepare to be called by force (the script) --- fix/wildlife.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/fix/wildlife.lua b/fix/wildlife.lua index cbd5e13b20..bd80f2433b 100644 --- a/fix/wildlife.lua +++ b/fix/wildlife.lua @@ -102,6 +102,17 @@ local function is_active_wildlife(unit) unit.flags2.roaming_wilderness_population_source end +-- called by force for the "Wildlife" event +function free_all_wildlife(include_hidden) + for _,unit in ipairs(df.global.world.units.active) do + if is_active_wildlife(unit) and + (include_hidden or not dfhack.units.isHidden(unit)) + then + detach_unit(unit) + end + end +end + local function unstick_surface_wildlife(opts) local unstuck = {} local week_ago_ticks = math.max(0, df.global.cur_year_tick - TICKS_PER_WEEK) From d698be43ebaa3ccff7bfa3fb0537db915e8541c2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 16:04:05 -0700 Subject: [PATCH 147/811] support Wildlife synthetic "event" merge modtools/force into force --- changelog.txt | 1 + docs/force.rst | 31 ++++++++++++++++++++++++------ force.lua | 52 ++++++++++++++++++++++++++++++++++++++------------ 3 files changed, 66 insertions(+), 18 deletions(-) diff --git a/changelog.txt b/changelog.txt index b58d36042f..a2d46d6fa7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools ## New Features +- `force`: support the ``Wildlife`` event to allow additional wildlife to enter the map ## Fixes - `gui/quickfort`: only print a help blueprint's text once even if the repeat setting is enabled diff --git a/docs/force.rst b/docs/force.rst index 915863a75f..5bc13bd7f6 100644 --- a/docs/force.rst +++ b/docs/force.rst @@ -15,6 +15,7 @@ Usage :: force [] + force Wildlife [all] The civ id is only used for ``Diplomat`` and ``Caravan`` events, and defaults to the player civilization if not specified. @@ -27,18 +28,36 @@ The default civ IDs that you are likely to be interested in are: But to see IDs for all civilizations in your current game, run this command:: - devel/query --table df.global.world.entities.all --search code --maxdepth 2 + :lua ids={} for _,en in ipairs(world.entities.all) do ids[en.entity_raw.code] = true end for id in pairs(ids) do print(id) end + +Examples +-------- + +``force Caravan`` + Spawn a caravan from your parent civilization. +``force Diplomat FOREST`` + Spawn an elven diplomat. +``force Megabeast`` + Call in a megabeast to attack your fort. The megabeast will enter the map + on the surface. +``force Wildlife`` + Allow additional wildlife to enter the map. Only affects areas that you can + see, so if you haven't opened the caverns, cavern wildlife won't be + affected. +``force Wildlife all`` + Allow additional wildlife to enter the map, even in areas you haven't + explored yet. Event types ----------- -The recognized event types are: +The supported event types are: - ``Caravan`` - ``Migrants`` - ``Diplomat`` - ``Megabeast`` -- ``WildlifeCurious`` -- ``WildlifeMischievous`` -- ``WildlifeFlier`` -- ``NightCreature`` +- ``Wildlife`` + +Most events happen on the next tick. The ``Wildlife`` event may take up to 14 +ticks to take effect. diff --git a/force.lua b/force.lua index 9360ca8b95..afb3c4febd 100644 --- a/force.lua +++ b/force.lua @@ -1,30 +1,58 @@ --- Forces an event (wrapper for modtools/force) +local wildlife = reqscript('fix/wildlife') -local utils = require 'utils' -local args = {...} +local function findCiv(civ) + if civ == 'player' then return df.historical_entity.find(df.global.plotinfo.civ_id) end + if tonumber(civ) then return df.historical_entity.find(tonumber(civ)) end + civ = string.lower(tostring(civ)) + for _,entity in ipairs(df.global.world.entities.all) do + if string.lower(entity.entity_raw.code) == civ then return entity end + end +end + +local args = { ... } if #args < 1 then qerror('missing event type') end if args[1]:find('help') then print(dfhack.script_help()) return end -local eventType = nil + +local eventType = args[1]:upper() + +-- handle synthetic events +if eventType == 'WILDLIFE' then + wildlife.free_all_wildlife(args[2] == 'all') + return +end + +-- handle native events for _, type in ipairs(df.timed_event_type) do if type:lower() == args[1]:lower() then eventType = type end end -if not eventType then +if not df.timed_event_type[eventType] then qerror('unknown event type: ' .. args[1]) end +if eventType == 'FeatureAttack' then + qerror('Event type: FeatureAttack is not currently supported') +end + +local civ -local newArgs = {'--eventType', eventType} if eventType == 'Caravan' or eventType == 'Diplomat' then - table.insert(newArgs, '--civ') - if not args[2] then - table.insert(newArgs, 'player') - else - table.insert(newArgs, args[2]) + civ = findCiv(args[2] or 'player') + if not civ then + qerror('unable to find civilization: '..tostring(civ)) end +elseif eventType == 'Migrants' then + civ = findCiv('player') end -dfhack.run_script('modtools/force', table.unpack(newArgs)) +df.global.timed_events:insert('#', { + new=true, + type=df.timed_event_type[eventType], + season=df.global.cur_season, + season_ticks=df.global.cur_season_tick, + entity=civ, + feature_ind=-1, +}) From 54c433436ce3621b0fae59f83606dafcbcf544dd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 16:04:56 -0700 Subject: [PATCH 148/811] remove modtools/force merged into the regular force command --- docs/modtools/force.rst | 32 --------------------- modtools/force.lua | 64 ----------------------------------------- 2 files changed, 96 deletions(-) delete mode 100644 docs/modtools/force.rst delete mode 100644 modtools/force.lua diff --git a/docs/modtools/force.rst b/docs/modtools/force.rst deleted file mode 100644 index 9f71183fa1..0000000000 --- a/docs/modtools/force.rst +++ /dev/null @@ -1,32 +0,0 @@ -modtools/force -============== - -.. dfhack-tool:: - :summary: Trigger game events. - :tags: dev - -This tool triggers events like megabeasts, caravans, and migrants. - -Usage ------ - -:: - - -eventType event - specify the type of the event to trigger - examples: - Megabeast - Migrants - Caravan - Diplomat - WildlifeCurious - WildlifeMischievous - WildlifeFlier - NightCreature - -civ entity - specify the civ of the event, if applicable - examples: - player - MOUNTAIN - EVIL - 28 diff --git a/modtools/force.lua b/modtools/force.lua deleted file mode 100644 index eb49b91552..0000000000 --- a/modtools/force.lua +++ /dev/null @@ -1,64 +0,0 @@ --- Forces an event (caravan, migrants, etc) --- author Putnam --- edited by expwnent -local utils = require 'utils' - -local function findCiv(arg) - local entities = df.global.world.entities.all - if tonumber(arg) then return df.historical_entity.find(tonumber(arg)) end - if arg then - for eid,entity in ipairs(entities) do - if entity.entity_raw.code == arg then return entity end - end - end - return nil -end - -local validArgs = utils.invert({ - 'eventType', - 'help', - 'civ' -}) - -local args = utils.processArgs({...}, validArgs) -if args.help then - print(dfhack.script_help()) - return -end - -if not args.eventType then - error 'Specify an eventType.' -elseif not df.timed_event_type[args.eventType] then - error('Invalid eventType: ' .. args.eventType) -elseif args.eventType == 'FeatureAttack' then - qerror('Event type: FeatureAttack is not currently supported') -end - -local civ = nil --as:df.historical_entity -if args.civ == 'player' then - civ = df.historical_entity.find(df.global.plotinfo.civ_id) -elseif args.civ then - civ = findCiv(args.civ) -end -if args.civ and not civ then - error('Invalid civ: ' .. args.civ) -end -if args.eventType == 'Caravan' or args.eventType == 'Diplomat' then - if not civ then - error('Specify civ for this eventType') - end -end - -if args.eventType == 'Migrants' then - civ = df.historical_entity.find(df.global.plotinfo.civ_id) -end - -local timedEvent = df.timed_event:new() -timedEvent.type = df.timed_event_type[args.eventType] -timedEvent.season = df.global.cur_season -timedEvent.season_ticks = df.global.cur_season_tick -if civ then - timedEvent.entity = civ -end - -df.global.timed_events:insert('#', timedEvent) From d2fc7c999379a25ac4322573fc96e056777c0598 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 16:05:28 -0700 Subject: [PATCH 149/811] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index a2d46d6fa7..e47da2ec92 100644 --- a/changelog.txt +++ b/changelog.txt @@ -41,6 +41,7 @@ Template for new versions: - `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. ## Removed +- `modtools/force`: merged into `force` # 50.13-r5 From 20cc47bfcb6012f46bf74c7a15bf991de26bb81a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 16:19:28 -0700 Subject: [PATCH 150/811] fix docs for latency of wildlife spawns --- docs/force.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/force.rst b/docs/force.rst index 5bc13bd7f6..fb52a15803 100644 --- a/docs/force.rst +++ b/docs/force.rst @@ -59,5 +59,5 @@ The supported event types are: - ``Megabeast`` - ``Wildlife`` -Most events happen on the next tick. The ``Wildlife`` event may take up to 14 +Most events happen on the next tick. The ``Wildlife`` event may take up to 100 ticks to take effect. From 2bf2c241b136086cdf6ebad366b59c3ebc4a35f9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 16:40:43 -0700 Subject: [PATCH 151/811] clear enemy status cache for makeown and allow fix/loyaltycascade to work on non-dwarves --- changelog.txt | 2 ++ docs/fix/loyaltycascade.rst | 2 +- fix/loyaltycascade.lua | 40 ++++++++++--------------------------- makeown.lua | 26 ++++++++++++++++++++++++ 4 files changed, 40 insertions(+), 30 deletions(-) diff --git a/changelog.txt b/changelog.txt index b58d36042f..4470beb018 100644 --- a/changelog.txt +++ b/changelog.txt @@ -32,6 +32,8 @@ Template for new versions: ## Fixes - `gui/quickfort`: only print a help blueprint's text once even if the repeat setting is enabled +- `makeown`: quell any active enemy relationships with the converted creature +- `fix/loyaltycascade`: allow the fix to work on non-dwarven citizens - `control-panel`: fix setting numeric preferences from the commandline ## Misc Improvements diff --git a/docs/fix/loyaltycascade.rst b/docs/fix/loyaltycascade.rst index 7a1eaad620..39899aae3f 100644 --- a/docs/fix/loyaltycascade.rst +++ b/docs/fix/loyaltycascade.rst @@ -5,7 +5,7 @@ fix/loyaltycascade :summary: Halts loyalty cascades where dwarves are fighting dwarves. :tags: fort bugfix units -This tool aborts loyalty cascades by fixing units who consider their own +This tool neutralizes loyalty cascades by fixing units who consider their own civilization to be the enemy. Usage diff --git a/fix/loyaltycascade.lua b/fix/loyaltycascade.lua index a3b7090880..b6ae0515ef 100644 --- a/fix/loyaltycascade.lua +++ b/fix/loyaltycascade.lua @@ -1,5 +1,7 @@ -- Prevents a "loyalty cascade" (intra-fort civil war) when a citizen is killed. +local makeown = reqscript('makeown') + -- Checks if a unit is a former member of a given entity as well as it's -- current enemy. local function getUnitRenegade(unit, entity_id) @@ -14,9 +16,9 @@ local function getUnitRenegade(unit, entity_id) goto skipentity end - if link_type == df.histfig_entity_link_type.FORMER_MEMBER then + if link_type == df.histfig_entity_link_type.FORMER_MEMBER then former_index = index - elseif link_type == df.histfig_entity_link_type.ENEMY then + elseif link_type == df.histfig_entity_link_type.ENEMY then enemy_index = index end @@ -42,11 +44,7 @@ end local function fixUnit(unit) local fixed = false - if not dfhack.units.isOwnCiv(unit) or not dfhack.units.isDwarf(unit) then - return fixed - end - - local unit_name = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) + local unit_name = dfhack.units.getReadableName(unit) local former_civ_index, enemy_civ_index = getUnitRenegade(unit, df.global.plotinfo.civ_id) local former_group_index, enemy_group_index = getUnitRenegade(unit, df.global.plotinfo.group_id) @@ -57,7 +55,8 @@ local function fixUnit(unit) convertUnit(unit, df.global.plotinfo.civ_id, former_civ_index, enemy_civ_index) - dfhack.gui.showAnnouncement(('loyaltycascade: %s is now a member of %s again'):format(unit_name, civ_name), COLOR_WHITE) + dfhack.gui.showAnnouncement( + ('loyaltycascade: %s is now a happy member of %s again'):format(unit_name, civ_name), COLOR_WHITE) fixed = true end @@ -67,31 +66,14 @@ local function fixUnit(unit) convertUnit(unit, df.global.plotinfo.group_id, former_group_index, enemy_group_index) - dfhack.gui.showAnnouncement(('loyaltycascade: %s is now a member of %s again'):format(unit_name, group_name), COLOR_WHITE) + dfhack.gui.showAnnouncement( + ('loyaltycascade: %s is now a happy member of %s again'):format(unit_name, group_name), COLOR_WHITE) fixed = true end - if fixed and unit.enemy.enemy_status_slot ~= -1 then - local status_cache = df.global.world.enemy_status_cache - local status_slot = unit.enemy.enemy_status_slot - - unit.enemy.enemy_status_slot = -1 - status_cache.slot_used[status_slot] = false - - for index, _ in pairs(status_cache.rel_map[status_slot]) do - status_cache.rel_map[status_slot][index] = -1 - end - - for index, _ in pairs(status_cache.rel_map) do - status_cache.rel_map[index][status_slot] = -1 - end - - -- TODO: what if there were status slots taken above status_slot? - -- does everything need to be moved down by one? - if status_cache.next_slot > status_slot then - status_cache.next_slot = status_slot - end + if fixed then + makeown.clear_enemy_status(unit) end return false diff --git a/makeown.lua b/makeown.lua index 565b7c6856..73054f6ed3 100644 --- a/makeown.lua +++ b/makeown.lua @@ -59,6 +59,30 @@ local function fix_clothing_ownership(unit) unit.uniform.uniform_drop:resize(0) end +function clear_enemy_status(unit) + if unit.enemy.enemy_status_slot <= -1 then return end + + local status_cache = df.global.world.enemy_status_cache + local status_slot = unit.enemy.enemy_status_slot + + unit.enemy.enemy_status_slot = -1 + status_cache.slot_used[status_slot] = false + + for index in ipairs(status_cache.rel_map[status_slot]) do + status_cache.rel_map[status_slot][index] = -1 + end + + for index in ipairs(status_cache.rel_map) do + status_cache.rel_map[index][status_slot] = -1 + end + + -- TODO: what if there were status slots taken above status_slot? + -- does everything need to be moved down by one to fill the gap? + if status_cache.next_slot > status_slot then + status_cache.next_slot = status_slot + end +end + local function fix_unit(unit) unit.flags1.marauder = false; unit.flags1.merchant = false; @@ -82,6 +106,8 @@ local function fix_unit(unit) if unit.profession == df.profession.MERCHANT then unit.profession = df.profession.TRADER end if unit.profession2 == df.profession.MERCHANT then unit.profession2 = df.profession.TRADER end + + clear_enemy_status(unit) end local function add_to_entity(hf, eid) From 59d0da8c5a8ddd1b7b7c47b330840ee26c94d014 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 22:33:57 -0700 Subject: [PATCH 152/811] remove converted units from current conflicts --- changelog.txt | 2 +- makeown.lua | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index 90278e7137..6f889e8168 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,7 +34,7 @@ Template for new versions: ## Fixes - `gui/quickfort`: only print a help blueprint's text once even if the repeat setting is enabled -- `makeown`: quell any active enemy relationships with the converted creature +- `makeown`: quell any active enemy or conflict relationships with converted creatures - `fix/loyaltycascade`: allow the fix to work on non-dwarven citizens - `control-panel`: fix setting numeric preferences from the commandline diff --git a/makeown.lua b/makeown.lua index 73054f6ed3..5ffc4bf7c2 100644 --- a/makeown.lua +++ b/makeown.lua @@ -83,6 +83,23 @@ function clear_enemy_status(unit) end end +local prof_map = { + [df.profession.MERCHANT]=df.profession.TRADER, + [df.profession.THIEF]=df.profession.STANDARD, + [df.profession.MASTER_THIEF]=df.profession.STANDARD, + [df.profession.CRIMINAL]=df.profession.STANDARD, + [df.profession.DRUNK]=df.profession.STANDARD, + [df.profession.MONSTER_SLAYER]=df.profession.STANDARD, + [df.profession.SCOUT]=df.profession.STANDARD, + [df.profession.BEAST_HUNTER]=df.profession.STANDARD, + [df.profession.SNATCHER]=df.profession.STANDARD, + [df.profession.MERCENARY]=df.profession.STANDARD, +} + +local function sanitize_profession(prof) + return prof_map[prof] or prof +end + local function fix_unit(unit) unit.flags1.marauder = false; unit.flags1.merchant = false; @@ -104,8 +121,34 @@ local function fix_unit(unit) unit.civ_id = df.global.plotinfo.civ_id; - if unit.profession == df.profession.MERCHANT then unit.profession = df.profession.TRADER end - if unit.profession2 == df.profession.MERCHANT then unit.profession2 = df.profession.TRADER end + unit.profession = sanitize_profession(unit.profession) + unit.profession2 = sanitize_profession(unit.profession2) + + unit.invasion_id = -1 + unit.enemy.army_controller_id = -1 + unit.enemy.army_controller = nil + + unit.relationship_ids.GroupLeader = -1 + for _,other in ipairs(df.global.world.units.active) do + if other.relationship_ids.GroupLeader == unit.id then + other.relationship_ids.GroupLeader = -1 + end + end + + -- remove unit from all current conflicts + unit.activities:resize(0) + for _,act in ipairs(df.global.world.activities.all) do + if act.type ~= df.activity_entry_type.Conflict then goto continue end + for _,ev in ipairs(act.events) do + if ev:getType() ~= df.activity_event_type.Conflict then goto next_ev end + for _,side in ipairs(ev.sides) do + utils.erase_sorted(side.histfig_ids, unit.hist_figure_id) + utils.erase_sorted(side.unit_ids, unit.id) + end + ::next_ev:: + end + ::continue:: + end clear_enemy_status(unit) end @@ -228,6 +271,8 @@ local function fix_histfig(unit) -- add them to our civ/site if they aren't already if not found_civlink then entity_link(hf, civ_id) end if not found_fortlink then entity_link(hf, group_id) end + + hf.profession = sanitize_profession(unit.profession) end ---@param unit df.unit From 085c03e677e9937aca1efe17991ba153c47cabc4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Oct 2024 23:50:51 -0700 Subject: [PATCH 153/811] cancel hostile jobs when makeown'd --- changelog.txt | 1 + makeown.lua | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/changelog.txt b/changelog.txt index 6f889e8168..3809982b04 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: ## Fixes - `gui/quickfort`: only print a help blueprint's text once even if the repeat setting is enabled - `makeown`: quell any active enemy or conflict relationships with converted creatures +- `makeown`: halt any hostile jobs the unit may be engaged in, like kidnapping - `fix/loyaltycascade`: allow the fix to work on non-dwarven citizens - `control-panel`: fix setting numeric preferences from the commandline diff --git a/makeown.lua b/makeown.lua index 5ffc4bf7c2..5265287253 100644 --- a/makeown.lua +++ b/makeown.lua @@ -100,6 +100,17 @@ local function sanitize_profession(prof) return prof_map[prof] or prof end +local hostile_jobs = utils.invert{ + df.job_type.Kidnap, + df.job_type.HeistItem, + df.job_type.AcceptHeistItem, +} + +local function cancel_hostile_jobs(job) + if not job or not hostile_jobs[job.job_type] then return end + dfhack.job.removeJob(job) +end + local function fix_unit(unit) unit.flags1.marauder = false; unit.flags1.merchant = false; @@ -151,6 +162,8 @@ local function fix_unit(unit) end clear_enemy_status(unit) + + cancel_hostile_jobs(unit.job.current_job) end local function add_to_entity(hf, eid) From 95bd9155e186b56666ed31510c366ae214fa024a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 5 Oct 2024 12:15:38 -0700 Subject: [PATCH 154/811] light doc editing --- docs/instruments.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/instruments.rst b/docs/instruments.rst index 815eb81b82..5165433b1f 100644 --- a/docs/instruments.rst +++ b/docs/instruments.rst @@ -5,7 +5,8 @@ instruments :summary: Show how to craft instruments or create work orders for them. :tags: fort inspection workorders -This tool is used to query information about instruments or to create work orders for them. +This tool is used to query information about instruments or to create work +orders to produce them. The ``list`` subcommand provides information on how to craft the instruments used by the player civilization. For single-piece instruments, it shows the @@ -15,7 +16,7 @@ necessary pieces. It also shows whether the instrument is handheld or placed as a building. The ``order`` subcommand is used to create work orders for an instrument and -all of it's parts. The final assemble instrument -order waits for the part +all of it's parts. The final assemble instrument order waits for the part orders to complete before starting. Usage @@ -38,10 +39,9 @@ Examples If the instrument named ``givel`` in your world has four components, this will create a total of 5 work orders: one for assembling 10 givels, and an order of 10 for each of the givel's parts. Instruments are randomly - generated, so your givel components may vary. - + generated, so your instrument names and components will vary. ``instruments order ilul`` - Creates work orders to assemble one ïlul. Spelling doesn't need to include + Creates work orders to assemble one ïlul. Spelling does not need to include the special ï character. Options From f5a85e2efbec274415f78d196b611afa2b551087 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 5 Oct 2024 16:46:33 -0700 Subject: [PATCH 155/811] better error message when failing to specify a feature or preset --- agitation-rebalance.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/agitation-rebalance.lua b/agitation-rebalance.lua index e5a9ad7fdc..37ba7925f4 100644 --- a/agitation-rebalance.lua +++ b/agitation-rebalance.lua @@ -326,7 +326,7 @@ end local function do_preset(preset_name) local preset = presets[preset_name] if not preset then - qerror('preset not found: ' .. preset_name) + qerror(('preset not found: "%s"'):format(preset_name)) end utils.assign(custom_difficulty, preset) print('agitation-rebalance: preset applied: ' .. preset_name) @@ -763,7 +763,7 @@ local function enable_feature(which, enabled) end local feature = state.features[which] if feature == nil then - qerror('feature not found: ' .. which) + qerror(('feature not found: "%s"'):format(which)) end state.features[which] = enabled print(('feature %sabled: %s'):format(enabled and 'en' or 'dis', which)) @@ -777,9 +777,9 @@ if dfhack_flags and dfhack_flags.enable then else do_disable() end elseif command == 'preset' then - do_preset(args[1]) + do_preset(args[1] or '') elseif command == 'enable' or command == 'disable' then - enable_feature(args[1], command == 'enable') + enable_feature(args[1] or '', command == 'enable') elseif not command or command == 'status' then print_status() return From b466050858cfc8425f8c662e14d12376849739f8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 5 Apr 2024 23:55:27 -0700 Subject: [PATCH 156/811] add skeleton and overlay link --- gui/manipulator.lua | 75 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) create mode 100644 gui/manipulator.lua diff --git a/gui/manipulator.lua b/gui/manipulator.lua new file mode 100644 index 0000000000..9e29e645aa --- /dev/null +++ b/gui/manipulator.lua @@ -0,0 +1,75 @@ +--@module = true + +local gui = require("gui") +local widgets = require("gui.widgets") +local overlay = require('plugins.overlay') + +------------------------ +-- Manipulator +-- + +Manipulator = defclass(Manipulator, widgets.Window) +Manipulator.ATTRS { + frame_title='Unit Overview and Manipulator', + frame={w=110, h=40}, + resizable=true, + resize_min={w=70, h=15}, +} + +function Manipulator:init() + self:addviews{ + } +end + +------------------------ +-- ManipulatorScreen +-- + +ManipulatorScreen = defclass(ManipulatorScreen, gui.ZScreen) +ManipulatorScreen.ATTRS { + focus_path='manipulator', +} + +function ManipulatorScreen:init() + self:addviews{Manipulator{}} +end + +function ManipulatorScreen:onDismiss() + view = nil +end + +------------------------ +-- ManipulatorOverlay +-- + +ManipulatorOverlay = defclass(ManipulatorOverlay, overlay.OverlayWidget) +ManipulatorOverlay.ATTRS{ + desc='Adds a hotkey to the vanilla units screen to launch the DFHack units interface.', + default_pos={x=50, y=-5}, + default_enabled=true, + viewscreens='dwarfmode/Info/CREATURES/CITIZEN', + frame={w=34, h=1}, +} + +function ManipulatorOverlay:init() + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0}, + label='DFHack citizen interface', + key='CUSTOM_CTRL_N', + on_activate=function() dfhack.run_script('gui/manipulator') end, + }, + } +end + +OVERLAY_WIDGETS = { + launcher=ManipulatorOverlay, +} + +if dfhack_flags.module then return end + +if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror("This script requires a fortress map to be loaded") +end + +view = view and view:raise() or ManipulatorScreen{}:show() From db1e07bc9a68324b3b39f9cc67a6eb6b6146424c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 6 Apr 2024 20:07:31 -0700 Subject: [PATCH 157/811] get basic display and scrolling working --- gui/manipulator.lua | 301 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 297 insertions(+), 4 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 9e29e645aa..37f3f48b8d 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -1,23 +1,316 @@ --@module = true local gui = require("gui") -local widgets = require("gui.widgets") +local json = require('json') local overlay = require('plugins.overlay') +local utils = require('utils') +local widgets = require("gui.widgets") + +local CONFIG_FILE = 'dfhack-config/manipulator.json' + +local config = json.open(CONFIG_FILE) + +------------------------ +-- Column +-- + +Column = defclass(Column, widgets.Panel) +Column.ATTRS{ + label='', + data_fn=DEFAULT_NIL, + count_fn=DEFAULT_NIL, + make_sort_order_fn=DEFAULT_NIL, + group=DEFAULT_NIL, + label_inset=0, + data_width=4, + hidden=DEFAULT_NIL, + autoarrange_subviews=true, +} + +function Column:init() + self.frame = utils.assign({t=0, b=0, l=0, w=14}, self.frame or {}) + + if not self.make_sort_order_fn then + self.make_sort_order_fn = function(unit_ids) + local spec = {key=function(choice) return self.data_fn(df.unit.find(choice.unit_id)) end} + return utils.make_sort_order(choices, {spec}) + end + end + + if self.hidden == nil then + self.hidden = safe_index(config.data, 'cols', self.label, 'hidden') + end + + self:addviews{ + widgets.Panel{ + frame={l=0, h=5}, + subviews={ + widgets.Divider{ + view_id='col_stem', + frame={l=self.label_inset, t=4, w=1, h=1}, + frame_style=gui.FRAME_INTERIOR, + frame_style_b=false, + }, + widgets.HotkeyLabel{ + view_id='col_label', + frame={l=self.label_inset, t=4}, + label=self.label, + on_activate=function() end, -- TODO: sort by this column + }, + }, + }, + widgets.Label{ + view_id='col_current', + frame={l=1+self.label_inset, w=4}, + }, + widgets.Label{ + view_id='col_total', + frame={l=1+self.label_inset, w=4}, + }, + widgets.List{ + view_id='col_list', + frame={l=0, w=self.data_width}, + }, + } + + self.subviews.col_list.scrollbar.visible = false +end + +function Column:set_data(units, unit_ids, sort_order) + self.unit_ids, self.sort_order = unit_ids, sort_order + + local choices = {} + local current, total = 0, 0 + local next_id_idx = 1 + for _, unit in ipairs(units) do + local val = self.count_fn(unit) + if unit.id == unit_ids[next_id_idx] then + local data = self.data_fn(unit) + table.insert(choices, (not data or data == 0) and '-' or tostring(data)) + current = current + val + next_id_idx = next_id_idx + 1 + end + total = total + val + end + self.subviews.col_current:setText(tostring(current)) + self.subviews.col_total:setText(tostring(total)) + self.subviews.col_list:setChoices(choices) +end + +function Column:set_stem_height(h) + self.subviews.col_label.frame.t = 4 - h + self.subviews.col_stem.frame.t = 4 - h + self.subviews.col_stem.frame.h = h + 1 +end + +------------------------ +-- DataColumn +-- + +DataColumn = defclass(DataColumn, Column) +DataColumn.ATTRS{ +} + +function DataColumn:init() + if not self.count_fn then + self.count_fn = function(unit) + local data = self.data_fn(unit) + if not data then return 0 end + if type(data) == 'number' then return data > 0 and 1 or 0 end + return 1 + end + end +end + +------------------------ +-- ToggleColumn +-- + +ToggleColumn = defclass(ToggleColumn, Column) +ToggleColumn.ATTRS{ + on_toggle=DEFAULT_NIL, +} + +function ToggleColumn:init() + if not self.count_fn then + self.count_fn = function(unit) return self.data_fn(unit) and 1 or 0 end + end +end + +------------------------ +-- Spreadsheet +-- + +Spreadsheet = defclass(Spreadsheet, widgets.Panel) + +function Spreadsheet:init() + self.left_col = 1 + + local cols = widgets.Panel{} + self.cols = cols + + cols:addviews{ + ToggleColumn{ + label='Favorites', + data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, + }, + } + + for i in ipairs(df.job_skill) do + local caption = df.job_skill.attrs[i].caption + if caption then + cols:addviews{ + DataColumn{ + label=caption, + data_fn=function(unit) + return (utils.binsearch(unit.status.current_soul.skills, i, 'id') or {rating=0}).rating + end, + group='skills', + } + } + end + end + + self:addviews{ + widgets.Label{ + frame={t=5, l=0}, + text='Shown:', + }, + widgets.Label{ + frame={t=6, l=0}, + text='Total:', + }, + DataColumn{ + view_id='name', + frame={w=30}, + label='Name', + label_inset=8, + data_fn=dfhack.units.getReadableName, + data_width=30, + }, + cols, + } + + self.list = self.subviews.name.subviews.col_list + self:addviews{ + widgets.Scrollbar{ + view_id='scrollbar', + frame={t=7, r=0}, + on_scroll=self.list:callback('on_scrollbar'), + } + } + self.list.scrollbar = self.subviews.scrollbar + + self:refresh() +end + +-- TODO: apply search and filtering +function Spreadsheet:get_visible_unit_ids(units) + local visible_unit_ids = {} + for _, unit in ipairs(units) do + table.insert(visible_unit_ids, unit.id) + end + return visible_unit_ids +end + +function Spreadsheet:update_col_layout(idx, col, width, max_width) + col.visible = not col.hidden and idx >= self.left_col and width + col.frame.w <= max_width + col.frame.l = width + return width + (col.visible and col.data_width+1 or 0) +end + +function Spreadsheet:refresh() + local units = dfhack.units.getCitizens() + local visible_unit_ids = self:get_visible_unit_ids(units) + --local sort_order = self.subviews.name.sort_order or self.subviews.name.make_sort_order_fn(visible_unit_ids) + local max_width = self.frame_body and self.frame_body.width or 0 + local ord, width = 1, self.subviews.name.data_width + 1 + self.subviews.name:set_data(units, visible_unit_ids, sort_order) + for idx, col in ipairs(self.cols.subviews) do + col:set_data(units, visible_unit_ids, sort_order) + if not col.hidden then + col:set_stem_height((6-ord)%5) + ord = ord + 1 + end + width = self:update_col_layout(idx, col, width, max_width) + end +end + +function Spreadsheet:preUpdateLayout(parent_rect) + local width = self.subviews.name.data_width + 1 + for idx, col in ipairs(self.cols.subviews) do + width = self:update_col_layout(idx, col, width, parent_rect.width) + end +end + +function Spreadsheet:render(dc) + local page_top = self.list.page_top + for idx, col in ipairs(self.cols.subviews) do + col.subviews.col_list.page_top = page_top + end + Spreadsheet.super.render(self, dc) +end ------------------------ -- Manipulator -- Manipulator = defclass(Manipulator, widgets.Window) -Manipulator.ATTRS { +Manipulator.ATTRS{ frame_title='Unit Overview and Manipulator', frame={w=110, h=40}, resizable=true, - resize_min={w=70, h=15}, + resize_min={w=70, h=25}, } function Manipulator:init() self:addviews{ + widgets.EditField{ + view_id='search', + frame={l=0, t=0}, + label_text='Search: ', + on_char=function(ch) return ch:match('[%l -]') end, + on_change=function() self.subviews.sheet:refresh() end, + }, + widgets.Divider{ + frame={l=0, r=0, t=2, h=1}, + frame_style=gui.FRAME_INTERIOR, + frame_style_l=false, + frame_style_r=false, + }, + Spreadsheet{ + view_id='sheet', + frame={l=0, t=3, r=0, b=7}, + }, + widgets.Divider{ + frame={l=0, r=0, b=6, h=1}, + frame_style=gui.FRAME_INTERIOR, + frame_style_l=false, + frame_style_r=false, + }, + widgets.Panel{ + frame={l=0, r=0, b=0, h=5}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text='Use arrow keys to navigate cells.', + }, + widgets.HotkeyLabel{ + frame={b=2, l=0}, + label='Sort/reverse sort by current column', + key='CUSTOM_SHIFT_S', + on_activate=function() end, -- TODO + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + auto_width=true, + label='Refresh', -- TODO add warning if citizen list has changed and needs refreshing + key='CUSTOM_SHIFT_R', + on_activate=function() end, -- TODO + }, + -- TODO moar hotkeys + }, + }, } end @@ -26,7 +319,7 @@ end -- ManipulatorScreen = defclass(ManipulatorScreen, gui.ZScreen) -ManipulatorScreen.ATTRS { +ManipulatorScreen.ATTRS{ focus_path='manipulator', } From 666af6173b8a9f9eaf48829aeb3ad3881b935015 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 9 Apr 2024 09:59:28 -0700 Subject: [PATCH 158/811] implement horizontal scanning and jumping to group labels --- gui/manipulator.lua | 206 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 168 insertions(+), 38 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 37f3f48b8d..a844e8b35f 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -20,11 +20,10 @@ Column.ATTRS{ data_fn=DEFAULT_NIL, count_fn=DEFAULT_NIL, make_sort_order_fn=DEFAULT_NIL, - group=DEFAULT_NIL, + group='', label_inset=0, data_width=4, hidden=DEFAULT_NIL, - autoarrange_subviews=true, } function Column:init() @@ -42,8 +41,14 @@ function Column:init() end self:addviews{ + widgets.TextButton{ + view_id='col_group', + frame={t=0, l=0, h=1, w=#self.group+2}, + label=self.group, + visible=#self.group > 0, + }, widgets.Panel{ - frame={l=0, h=5}, + frame={t=2, l=0, h=5}, subviews={ widgets.Divider{ view_id='col_stem', @@ -61,32 +66,33 @@ function Column:init() }, widgets.Label{ view_id='col_current', - frame={l=1+self.label_inset, w=4}, + frame={t=7, l=1+self.label_inset, w=4}, }, widgets.Label{ view_id='col_total', - frame={l=1+self.label_inset, w=4}, + frame={t=8, l=1+self.label_inset, w=4}, }, widgets.List{ view_id='col_list', - frame={l=0, w=self.data_width}, + frame={t=10, l=0, w=self.data_width}, }, } self.subviews.col_list.scrollbar.visible = false end -function Column:set_data(units, unit_ids, sort_order) - self.unit_ids, self.sort_order = unit_ids, sort_order - +function Column:set_data(units, visible_unit_ids) local choices = {} local current, total = 0, 0 local next_id_idx = 1 for _, unit in ipairs(units) do local val = self.count_fn(unit) - if unit.id == unit_ids[next_id_idx] then + if unit.id == visible_unit_ids[next_id_idx] then local data = self.data_fn(unit) - table.insert(choices, (not data or data == 0) and '-' or tostring(data)) + table.insert(choices, { + text=(not data or data == 0) and '-' or tostring(data), + unit_id=unit.id, + }) current = current + val next_id_idx = next_id_idx + 1 end @@ -153,6 +159,7 @@ function Spreadsheet:init() ToggleColumn{ label='Favorites', data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, + group='tags', }, } @@ -171,13 +178,35 @@ function Spreadsheet:init() end end + for _, wd in ipairs(df.global.plotinfo.labor_info.work_details) do + cols:addviews{ + ToggleColumn{ + label=wd.name, + data_fn=function(unit) + return utils.binsearch(wd.assigned_units, unit.id) and true or false + end, + group='work details', + } + } + end + self:addviews{ + widgets.TextButton{ + view_id='left_group', + frame={t=0, l=0, h=1}, + visible=false, + }, + widgets.TextButton{ + view_id='right_group', + frame={t=0, r=0, h=1}, + visible=false, + }, widgets.Label{ - frame={t=5, l=0}, + frame={t=7, l=0}, text='Shown:', }, widgets.Label{ - frame={t=6, l=0}, + frame={t=8, l=0}, text='Total:', }, DataColumn{ @@ -213,44 +242,105 @@ function Spreadsheet:get_visible_unit_ids(units) return visible_unit_ids end -function Spreadsheet:update_col_layout(idx, col, width, max_width) - col.visible = not col.hidden and idx >= self.left_col and width + col.frame.w <= max_width - col.frame.l = width - return width + (col.visible and col.data_width+1 or 0) +function Spreadsheet:sort_by_current_row() +end + +function Spreadsheet:filter(search) +end + +function Spreadsheet:hide_current_row() +end + +function Spreadsheet:jump_to_group(group) + for i, col in ipairs(self.cols.subviews) do + if not col.hidden and col.group == group then + self.left_col = i + break + end + end + self:updateLayout() end function Spreadsheet:refresh() local units = dfhack.units.getCitizens() local visible_unit_ids = self:get_visible_unit_ids(units) --local sort_order = self.subviews.name.sort_order or self.subviews.name.make_sort_order_fn(visible_unit_ids) - local max_width = self.frame_body and self.frame_body.width or 0 - local ord, width = 1, self.subviews.name.data_width + 1 - self.subviews.name:set_data(units, visible_unit_ids, sort_order) - for idx, col in ipairs(self.cols.subviews) do - col:set_data(units, visible_unit_ids, sort_order) + local ord = 1 + self.subviews.name:set_data(units, visible_unit_ids) + for _, col in ipairs(self.cols.subviews) do + col:set_data(units, visible_unit_ids) if not col.hidden then - col:set_stem_height((6-ord)%5) + col:set_stem_height((5-ord)%5) ord = ord + 1 end - width = self:update_col_layout(idx, col, width, max_width) + end + if (self.frame_parent_rect) then + self:updateLayout() end end +function Spreadsheet:update_col_layout(idx, col, width, group, max_width) + col.visible = not col.hidden and idx >= self.left_col and width + col.frame.w <= max_width + col.frame.l = width + if not col.visible then + return width, group + end + local col_group = col.subviews.col_group + col_group.label.on_activate=self:callback('jump_to_group', col.group) + col_group.visible = group ~= col.group + return width + col.data_width + 1, col.group +end + function Spreadsheet:preUpdateLayout(parent_rect) - local width = self.subviews.name.data_width + 1 + local left_group, right_group = self.subviews.left_group, self.subviews.right_group + left_group.visible, right_group.visible = false, false + + local width, group, cur_col_group = self.subviews.name.data_width + 1, '', '' + local prev_col_group, next_col_group for idx, col in ipairs(self.cols.subviews) do - width = self:update_col_layout(idx, col, width, parent_rect.width) + local prev_group = group + width, group = self:update_col_layout(idx, col, width, group, parent_rect.width) + if not next_col_group and group ~= '' and not col.visible and col.group ~= cur_col_group then + next_col_group = col.group + local str = next_col_group .. string.char(26) -- right arrow + right_group:setLabel(str) + right_group.frame.w = #str + 2 + right_group.label.on_activate=self:callback('jump_to_group', next_col_group) + right_group.visible = true + end + if cur_col_group ~= col.group then + prev_col_group = cur_col_group + end + cur_col_group = col.group + if prev_group == '' and group ~= '' and prev_col_group and prev_col_group ~= '' then + local str = string.char(27) .. prev_col_group -- left arrow + left_group:setLabel(str) + left_group.frame.w = #str + 2 + left_group.label.on_activate=self:callback('jump_to_group', prev_col_group) + left_group.visible = true + end end end function Spreadsheet:render(dc) local page_top = self.list.page_top - for idx, col in ipairs(self.cols.subviews) do + for _, col in ipairs(self.cols.subviews) do col.subviews.col_list.page_top = page_top end Spreadsheet.super.render(self, dc) end +function Spreadsheet:onInput(keys) + if keys.KEYBOARD_CURSOR_LEFT then + self.left_col = math.max(1, self.left_col - 1) + self:updateLayout() + elseif keys.KEYBOARD_CURSOR_RIGHT then + self.left_col = math.min(#self.cols.subviews, self.left_col + 1) + self:updateLayout() + end + return Spreadsheet.super.onInput(self, keys) +end + ------------------------ -- Manipulator -- @@ -259,8 +349,9 @@ Manipulator = defclass(Manipulator, widgets.Window) Manipulator.ATTRS{ frame_title='Unit Overview and Manipulator', frame={w=110, h=40}, + frame_inset={t=1, l=1, r=1, b=0}, resizable=true, - resize_min={w=70, h=25}, + resize_min={w=70, h=30}, } function Manipulator:init() @@ -268,9 +359,9 @@ function Manipulator:init() widgets.EditField{ view_id='search', frame={l=0, t=0}, + key='FILTER', label_text='Search: ', - on_char=function(ch) return ch:match('[%l -]') end, - on_change=function() self.subviews.sheet:refresh() end, + on_change=function(val) self.subviews.sheet:filter(val) end, }, widgets.Divider{ frame={l=0, r=0, t=2, h=1}, @@ -291,24 +382,63 @@ function Manipulator:init() widgets.Panel{ frame={l=0, r=0, b=0, h=5}, subviews={ - widgets.Label{ + widgets.WrappedLabel{ frame={t=0, l=0}, - text='Use arrow keys to navigate cells.', + text_to_wrap='Use arrow keys or middle click drag to navigate cells. Left click or ENTER to toggle current cell.', }, - widgets.HotkeyLabel{ + widgets.Label{ frame={b=2, l=0}, - label='Sort/reverse sort by current column', + text='Current column:', + }, + widgets.HotkeyLabel{ + frame={b=2, l=17}, + auto_width=true, + label='Sort/reverse sort', key='CUSTOM_SHIFT_S', - on_activate=function() end, -- TODO + on_activate=function() self.subviews.sheet:sort_by_current_row() end, + }, + widgets.HotkeyLabel{ + frame={b=2, l=39}, + auto_width=true, + label='Hide', + key='CUSTOM_SHIFT_H', + on_activate=function() self.subviews.sheet:hide_current_row() end, + }, + widgets.Label{ + frame={b=1, l=0}, + text='Current group:', + }, + widgets.HotkeyLabel{ + frame={b=1, l=17}, + auto_width=true, + label='Next group', + key='CUSTOM_CTRL_T', + on_activate=function() end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=37}, + auto_width=true, + label='Hide', + key='CUSTOM_CTRL_H', + on_activate=function() self.subviews.sheet:hide_current_row() end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=51}, + auto_width=true, + label='Show hidden', + key='CUSTOM_CTRL_W', + on_activate=function() self.subviews.sheet:hide_current_row() end, }, widgets.HotkeyLabel{ frame={b=0, l=0}, auto_width=true, - label='Refresh', -- TODO add warning if citizen list has changed and needs refreshing + label='Refresh', -- TODO: add warning if citizen list has changed and needs refreshing key='CUSTOM_SHIFT_R', - on_activate=function() end, -- TODO + on_activate=function() + self.subviews.sheet:refresh() + self.subviews.sheet:filter(self.subviews.search.text) + end, }, - -- TODO moar hotkeys }, }, } From fd263b537a8aeda028a088496e05fe8a381f9c8f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 10 Apr 2024 18:08:00 -0700 Subject: [PATCH 159/811] beginning infrastructure for sorting --- gui/manipulator.lua | 165 +++++++++++++++++++++++++++++++------------- 1 file changed, 117 insertions(+), 48 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index a844e8b35f..5c21d85b51 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -8,7 +8,7 @@ local widgets = require("gui.widgets") local CONFIG_FILE = 'dfhack-config/manipulator.json' -local config = json.open(CONFIG_FILE) +config = config or json.open(CONFIG_FILE) ------------------------ -- Column @@ -16,30 +16,21 @@ local config = json.open(CONFIG_FILE) Column = defclass(Column, widgets.Panel) Column.ATTRS{ - label='', - data_fn=DEFAULT_NIL, - count_fn=DEFAULT_NIL, - make_sort_order_fn=DEFAULT_NIL, + idx=DEFAULT_NIL, + label=DEFAULT_NIL, group='', label_inset=0, data_width=4, hidden=DEFAULT_NIL, + shared=DEFAULT_NIL, + data_fn=DEFAULT_NIL, + count_fn=DEFAULT_NIL, + cmp_fn=DEFAULT_NIL, } function Column:init() self.frame = utils.assign({t=0, b=0, l=0, w=14}, self.frame or {}) - if not self.make_sort_order_fn then - self.make_sort_order_fn = function(unit_ids) - local spec = {key=function(choice) return self.data_fn(df.unit.find(choice.unit_id)) end} - return utils.make_sort_order(choices, {spec}) - end - end - - if self.hidden == nil then - self.hidden = safe_index(config.data, 'cols', self.label, 'hidden') - end - self:addviews{ widgets.TextButton{ view_id='col_group', @@ -60,7 +51,7 @@ function Column:init() view_id='col_label', frame={l=self.label_inset, t=4}, label=self.label, - on_activate=function() end, -- TODO: sort by this column + on_activate=self:callback('sort'), }, }, }, @@ -79,28 +70,75 @@ function Column:init() } self.subviews.col_list.scrollbar.visible = false + self.dirty = true end -function Column:set_data(units, visible_unit_ids) - local choices = {} +function Column:sort() + if self.dirty then + self:refresh() + end + if self.shared.sort_idx == self.idx then + self.shared.sort_rev = not self.shared.sort_rev + else + self.shared.sort_idx = self.idx + self.shared.sort_rev = false + end + local spec = {compare=self.cmp_fn, reverse=self.shared.sort_rev, key=function(choice) return choice.data end} + local ordered_col_data = {} + for i, ordered_i in ipairs(self.shared.sort_order) do + end + local sort_order = utils.make_sort_order(ordered_col_data, {spec}) +end + +function Column:get_units() + if self.shared.cache.units then return self.shared.cache.units end + local units = {} + for _, unit_id in ipairs(self.shared.unit_ids) do + local unit = df.unit.find(unit_id) + if unit then + table.insert(units, unit) + else + self.shared.fault = true + end + end + self.shared.cache.units = units + return units +end + +function Column:refresh() + local col_data, choices = {}, {} local current, total = 0, 0 local next_id_idx = 1 - for _, unit in ipairs(units) do - local val = self.count_fn(unit) - if unit.id == visible_unit_ids[next_id_idx] then - local data = self.data_fn(unit) + for _, unit in ipairs(self:get_units()) do + local data = self.data_fn(unit) + local val = self.count_fn(data) + if unit.id == self.shared.filtered_unit_ids[next_id_idx] then + table.insert(col_data, data) table.insert(choices, { - text=(not data or data == 0) and '-' or tostring(data), - unit_id=unit.id, + text=function() + local ordered_data = col_data[self.shared.sort_order[next_id_idx]] + return (not data or data == 0) and '-' or tostring(data) + end, }) current = current + val next_id_idx = next_id_idx + 1 end total = total + val end + + self.col_data = col_data self.subviews.col_current:setText(tostring(current)) self.subviews.col_total:setText(tostring(total)) self.subviews.col_list:setChoices(choices) + + self.dirty = false +end + +function Column:render(dc) + if self.dirty then + self:refresh() + end + Column.super.render(self, dc) end function Column:set_stem_height(h) @@ -119,8 +157,7 @@ DataColumn.ATTRS{ function DataColumn:init() if not self.count_fn then - self.count_fn = function(unit) - local data = self.data_fn(unit) + self.count_fn = function(data) if not data then return 0 end if type(data) == 'number' then return data > 0 and 1 or 0 end return 1 @@ -139,7 +176,7 @@ ToggleColumn.ATTRS{ function ToggleColumn:init() if not self.count_fn then - self.count_fn = function(unit) return self.data_fn(unit) and 1 or 0 end + self.count_fn = function(data) return data and 1 or 0 end end end @@ -151,15 +188,21 @@ Spreadsheet = defclass(Spreadsheet, widgets.Panel) function Spreadsheet:init() self.left_col = 1 + self.dirty = true + + self.shared = {sort_idx=-1, sort_rev=false, cache={}, unit_ids={}, filtered_unit_ids={}, sort_order={}} local cols = widgets.Panel{} self.cols = cols cols:addviews{ ToggleColumn{ + view_id='favorites', + idx=#cols.subviews+1, label='Favorites', data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, group='tags', + shared=self.shared, }, } @@ -168,11 +211,13 @@ function Spreadsheet:init() if caption then cols:addviews{ DataColumn{ + idx=#cols.subviews+1, label=caption, data_fn=function(unit) return (utils.binsearch(unit.status.current_soul.skills, i, 'id') or {rating=0}).rating end, group='skills', + shared=self.shared, } } end @@ -181,11 +226,13 @@ function Spreadsheet:init() for _, wd in ipairs(df.global.plotinfo.labor_info.work_details) do cols:addviews{ ToggleColumn{ + idx=#cols.subviews+1, label=wd.name, data_fn=function(unit) return utils.binsearch(wd.assigned_units, unit.id) and true or false end, group='work details', + shared=self.shared, } } end @@ -193,12 +240,12 @@ function Spreadsheet:init() self:addviews{ widgets.TextButton{ view_id='left_group', - frame={t=0, l=0, h=1}, + frame={t=1, l=0, h=1}, visible=false, }, widgets.TextButton{ view_id='right_group', - frame={t=0, r=0, h=1}, + frame={t=1, r=0, h=1}, visible=false, }, widgets.Label{ @@ -212,10 +259,12 @@ function Spreadsheet:init() DataColumn{ view_id='name', frame={w=30}, + idx=0, label='Name', label_inset=8, data_fn=dfhack.units.getReadableName, data_width=30, + shared=self.shared, }, cols, } @@ -230,16 +279,7 @@ function Spreadsheet:init() } self.list.scrollbar = self.subviews.scrollbar - self:refresh() -end - --- TODO: apply search and filtering -function Spreadsheet:get_visible_unit_ids(units) - local visible_unit_ids = {} - for _, unit in ipairs(units) do - table.insert(visible_unit_ids, unit.id) - end - return visible_unit_ids + self:update_headers() end function Spreadsheet:sort_by_current_row() @@ -261,22 +301,46 @@ function Spreadsheet:jump_to_group(group) self:updateLayout() end -function Spreadsheet:refresh() - local units = dfhack.units.getCitizens() - local visible_unit_ids = self:get_visible_unit_ids(units) - --local sort_order = self.subviews.name.sort_order or self.subviews.name.make_sort_order_fn(visible_unit_ids) +function Spreadsheet:update_headers() local ord = 1 - self.subviews.name:set_data(units, visible_unit_ids) for _, col in ipairs(self.cols.subviews) do - col:set_data(units, visible_unit_ids) if not col.hidden then col:set_stem_height((5-ord)%5) ord = ord + 1 end end - if (self.frame_parent_rect) then - self:updateLayout() +end + +-- TODO: apply search and filtering +function Spreadsheet:get_visible_units(units) + local visible_units, visible_unit_ids = {}, {} + for _, unit in ipairs(units) do + table.insert(visible_units, unit) + table.insert(visible_unit_ids, unit.id) + end + return visible_units, visible_unit_ids +end + +function Spreadsheet:refresh() + self.shared.fault = false + self.subviews.name.dirty = true + for _, col in ipairs(self.cols.subviews) do + col.dirty = true + end + local units = dfhack.units.getCitizens() + self.shared.cache.units = units + self.shared.cache.visible_units, self.shared.visible_unit_ids = self:get_visible_units(units) + local sort_idx = self.shared.sort_idx + self.shared.sort_rev = not self.shared.sort_rev + if sort_idx == -1 then + self.subviews.name:sort() + self.subviews.favorites:sort() + elseif sort_idx == 0 then + self.subviews.name:sort() + else + self.cols.subviews[sort_idx]:sort() end + self.dirty = false end function Spreadsheet:update_col_layout(idx, col, width, group, max_width) @@ -323,11 +387,16 @@ function Spreadsheet:preUpdateLayout(parent_rect) end function Spreadsheet:render(dc) + if self.dirty or self.shared.fault then + self:refresh() + self:updateLayout() + end local page_top = self.list.page_top for _, col in ipairs(self.cols.subviews) do col.subviews.col_list.page_top = page_top end Spreadsheet.super.render(self, dc) + self.shared.cache = {} end function Spreadsheet:onInput(keys) From 71cd34fd7da64e35273bddf1627606da5d8f1313 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 10 May 2024 18:28:55 -0700 Subject: [PATCH 160/811] implement refresh hint --- gui/manipulator.lua | 149 +++++++++++++++++++++++++++---- internal/manipulator/presets.lua | 10 +++ 2 files changed, 142 insertions(+), 17 deletions(-) create mode 100644 internal/manipulator/presets.lua diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 5c21d85b51..0df0cd8498 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -3,12 +3,69 @@ local gui = require("gui") local json = require('json') local overlay = require('plugins.overlay') +local presets = reqscript('internal/manipulator/presets') local utils = require('utils') local widgets = require("gui.widgets") +------------------------ +-- persistent state +-- + +local GLOBAL_KEY = 'manipulator' local CONFIG_FILE = 'dfhack-config/manipulator.json' -config = config or json.open(CONFIG_FILE) +-- persistent player (global) state schema +local function get_default_config() + return { + tags={}, + presets={}, + } +end + +-- persistent per-fort state schema +local function get_default_state() + return { + favorites={}, + tagged={}, + } +end + +-- preset schema +local function get_default_preset() + return { + hidden_groups={}, + hidden_cols={}, + pinned={}, + } +end + +local function get_config() + local data = get_default_config() + local cfg = json.open(CONFIG_FILE) + utils.assign(data, cfg.data) + cfg.data = data + return cfg +end + +config = config or get_config() +state = state or get_default_state() +preset = preset or get_default_preset() + +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + state = get_default_state() + return + end + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return + end + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) +end ------------------------ -- Column @@ -185,6 +242,9 @@ end -- Spreadsheet = defclass(Spreadsheet, widgets.Panel) +Spreadsheet.ATTRS{ + get_units_fn=DEFAULT_NIL, +} function Spreadsheet:init() self.left_col = 1 @@ -403,9 +463,15 @@ function Spreadsheet:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then self.left_col = math.max(1, self.left_col - 1) self:updateLayout() + elseif keys.KEYBOARD_CURSOR_LEFT_FAST then + self.left_col = math.max(1, self.left_col - 10) + self:updateLayout() elseif keys.KEYBOARD_CURSOR_RIGHT then self.left_col = math.min(#self.cols.subviews, self.left_col + 1) self:updateLayout() + elseif keys.KEYBOARD_CURSOR_RIGHT_FAST then + self.left_col = math.min(#self.cols.subviews, self.left_col + 10) + self:updateLayout() end return Spreadsheet.super.onInput(self, keys) end @@ -414,6 +480,8 @@ end -- Manipulator -- +local REFRESH_MS = 1000 + Manipulator = defclass(Manipulator, widgets.Window) Manipulator.ATTRS{ frame_title='Unit Overview and Manipulator', @@ -424,6 +492,17 @@ Manipulator.ATTRS{ } function Manipulator:init() + if dfhack.world.isFortressMode() then + self.get_units_fn = dfhack.units.getCitizens + elseif dfhack.world.isAdventureMode() then + self.get_units_fn = qerror('get party members') + else + self.get_units_fn = function() return utils.clone(df.global.world.units.active) end + end + + self.needs_refresh, self.prev_unit_count, self.prev_last_unit_id = false, 0, -1 + self:update_needs_refresh(true) + self:addviews{ widgets.EditField{ view_id='search', @@ -441,6 +520,7 @@ function Manipulator:init() Spreadsheet{ view_id='sheet', frame={l=0, t=3, r=0, b=7}, + get_units_fn=self.get_units_fn, }, widgets.Divider{ frame={l=0, r=0, b=6, h=1}, @@ -464,14 +544,14 @@ function Manipulator:init() auto_width=true, label='Sort/reverse sort', key='CUSTOM_SHIFT_S', - on_activate=function() self.subviews.sheet:sort_by_current_row() end, + on_activate=function() self.subviews.sheet:sort_by_current_col() end, }, widgets.HotkeyLabel{ frame={b=2, l=39}, auto_width=true, label='Hide', key='CUSTOM_SHIFT_H', - on_activate=function() self.subviews.sheet:hide_current_row() end, + on_activate=function() self.subviews.sheet:hide_current_col() end, }, widgets.Label{ frame={b=1, l=0}, @@ -480,28 +560,33 @@ function Manipulator:init() widgets.HotkeyLabel{ frame={b=1, l=17}, auto_width=true, - label='Next group', - key='CUSTOM_CTRL_T', - on_activate=function() end, + label='Prev group', + key='CUSTOM_CTRL_Y', + on_activate=function() self.subviews.sheet:zoom_to_prev_group() end, }, widgets.HotkeyLabel{ frame={b=1, l=37}, auto_width=true, - label='Hide', - key='CUSTOM_CTRL_H', - on_activate=function() self.subviews.sheet:hide_current_row() end, + label='Next group', + key='CUSTOM_CTRL_T', + on_activate=function() self.subviews.sheet:zoom_to_next_group() end, }, widgets.HotkeyLabel{ - frame={b=1, l=51}, + frame={b=1, l=54}, auto_width=true, - label='Show hidden', - key='CUSTOM_CTRL_W', - on_activate=function() self.subviews.sheet:hide_current_row() end, + label='Hide', + key='CUSTOM_CTRL_H', + on_activate=function() self.subviews.sheet:hide_current_col_group() end, }, widgets.HotkeyLabel{ frame={b=0, l=0}, auto_width=true, - label='Refresh', -- TODO: add warning if citizen list has changed and needs refreshing + label=function() + return self.needs_refresh and 'Refresh (unit list has changed)' or 'Refresh' + end, + text_pen=function() + return self.needs_refresh and COLOR_LIGHTRED or nil + end, key='CUSTOM_SHIFT_R', on_activate=function() self.subviews.sheet:refresh() @@ -513,6 +598,36 @@ function Manipulator:init() } end +function Manipulator:update_needs_refresh(initialize) + self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS + + local units = self.get_units_fn() + local unit_count = #units + if unit_count ~= self.prev_unit_count then + self.needs_refresh = true + self.prev_unit_count = unit_count + end + if unit_count <= 0 then + self.prev_last_unit_id = -1 + else + local last_unit_id = units[#units] + if last_unit_id ~= self.prev_last_unit_id then + self.needs_refresh = true + self.prev_last_unit_id = last_unit_id + end + end + if initialize then + self.needs_refresh = false + end +end + +function Manipulator:render(dc) + if self.next_refresh_ms <= dfhack.getTickCount() then + self:update_needs_refresh() + end + Manipulator.super.render(self, dc) +end + ------------------------ -- ManipulatorScreen -- @@ -537,7 +652,7 @@ end ManipulatorOverlay = defclass(ManipulatorOverlay, overlay.OverlayWidget) ManipulatorOverlay.ATTRS{ desc='Adds a hotkey to the vanilla units screen to launch the DFHack units interface.', - default_pos={x=50, y=-5}, + default_pos={x=50, y=-6}, default_enabled=true, viewscreens='dwarfmode/Info/CREATURES/CITIZEN', frame={w=34, h=1}, @@ -560,8 +675,8 @@ OVERLAY_WIDGETS = { if dfhack_flags.module then return end -if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then - qerror("This script requires a fortress map to be loaded") +if not dfhack.isMapLoaded() then + qerror("This script requires a map to be loaded") end view = view and view:raise() or ManipulatorScreen{}:show() diff --git a/internal/manipulator/presets.lua b/internal/manipulator/presets.lua new file mode 100644 index 0000000000..757e625cda --- /dev/null +++ b/internal/manipulator/presets.lua @@ -0,0 +1,10 @@ +--@module=true + +PRESETS = { + { + name='', + groups={ + + }, + }, +} \ No newline at end of file From be4f488ed9fb8b3d2fe444cdef4dcd1dbb84a339 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 May 2024 15:14:24 -0700 Subject: [PATCH 161/811] shift scan by horizontal pages --- gui/manipulator.lua | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 0df0cd8498..6237f21d5d 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -459,18 +459,28 @@ function Spreadsheet:render(dc) self.shared.cache = {} end +function Spreadsheet:get_num_visible_cols() + local count = 0 + for _,col in ipairs(self.cols.subviews) do + if col.visible then + count = count + 1 + end + end + return count +end + function Spreadsheet:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then self.left_col = math.max(1, self.left_col - 1) self:updateLayout() elseif keys.KEYBOARD_CURSOR_LEFT_FAST then - self.left_col = math.max(1, self.left_col - 10) + self.left_col = math.max(1, self.left_col - self:get_num_visible_cols()) self:updateLayout() elseif keys.KEYBOARD_CURSOR_RIGHT then self.left_col = math.min(#self.cols.subviews, self.left_col + 1) self:updateLayout() elseif keys.KEYBOARD_CURSOR_RIGHT_FAST then - self.left_col = math.min(#self.cols.subviews, self.left_col + 10) + self.left_col = math.min(#self.cols.subviews, self.left_col + self:get_num_visible_cols()) self:updateLayout() end return Spreadsheet.super.onInput(self, keys) From 76777c18129bf6fadddb9ef2881efe4507d6ea72 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 May 2024 23:29:12 -0700 Subject: [PATCH 162/811] get sorting working (i think) --- gui/manipulator.lua | 150 +++++++++++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 37 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 6237f21d5d..ba4d33f25e 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -108,7 +108,7 @@ function Column:init() view_id='col_label', frame={l=self.label_inset, t=4}, label=self.label, - on_activate=self:callback('sort'), + on_activate=self:callback('sort', true), }, }, }, @@ -127,24 +127,52 @@ function Column:init() } self.subviews.col_list.scrollbar.visible = false + self.col_data = {} self.dirty = true end -function Column:sort() +function Column:sort(make_primary) if self.dirty then self:refresh() end - if self.shared.sort_idx == self.idx then - self.shared.sort_rev = not self.shared.sort_rev - else - self.shared.sort_idx = self.idx - self.shared.sort_rev = false + local sort_stack = self.shared.sort_stack + if make_primary then + -- we are newly sorting by this column: reverse sort if we're already on top of the + -- stack; otherwise put us on top of the stack + local top = sort_stack[#sort_stack] + if top.col == self then + top.rev = not top.rev + else + for idx,sort_spec in ipairs(sort_stack) do + if sort_spec.col == self then + table.remove(sort_stack, idx) + break + end + end + table.insert(sort_stack, {col=self, rev=false}) + end end - local spec = {compare=self.cmp_fn, reverse=self.shared.sort_rev, key=function(choice) return choice.data end} - local ordered_col_data = {} - for i, ordered_i in ipairs(self.shared.sort_order) do + local compare = function(a, b) + for idx=#sort_stack,1,-1 do + local sort_spec = sort_stack[idx] + local col = sort_spec.col + local first, second + if sort_spec.rev then + first, second = col.col_data[b], col.col_data[a] + else + first, second = col.col_data[a], col.col_data[b] + end + if first == second then goto continue end + if not first then return 1 end + if not second then return -1 end + local ret = (col.cmp_fn or utils.compare)(first, second) + if ret ~= 0 then return ret end + ::continue:: + end + return 0 end - local sort_order = utils.make_sort_order(ordered_col_data, {spec}) + local spec = {compare=compare} + self.shared.sort_order = utils.make_sort_order(self.shared.sort_order, {spec}) end function Column:get_units() @@ -172,10 +200,12 @@ function Column:refresh() if unit.id == self.shared.filtered_unit_ids[next_id_idx] then table.insert(col_data, data) table.insert(choices, { - text=function() - local ordered_data = col_data[self.shared.sort_order[next_id_idx]] - return (not data or data == 0) and '-' or tostring(data) - end, + { + text=function() + local ordered_data = col_data[self.shared.sort_order[next_id_idx]] + return (not ordered_data or ordered_data == 0) and '-' or tostring(ordered_data) + end, + }, }) current = current + val next_id_idx = next_id_idx + 1 @@ -250,7 +280,13 @@ function Spreadsheet:init() self.left_col = 1 self.dirty = true - self.shared = {sort_idx=-1, sort_rev=false, cache={}, unit_ids={}, filtered_unit_ids={}, sort_order={}} + self.shared = { + unit_ids={}, + filtered_unit_ids={}, + sort_stack={}, + sort_order={}, -- list of indices into filtered_unit_ids (or cache.filtered_units) + cache={}, -- cached pointers; reset at end of frame + } local cols = widgets.Panel{} self.cols = cols @@ -258,12 +294,17 @@ function Spreadsheet:init() cols:addviews{ ToggleColumn{ view_id='favorites', - idx=#cols.subviews+1, label='Favorites', data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, group='tags', shared=self.shared, }, + DataColumn{ + label='Stress', + data_fn=function(unit) return unit.status.current_soul.personality.stress end, + group='summary', + shared=self.shared, + } } for i in ipairs(df.job_skill) do @@ -271,7 +312,6 @@ function Spreadsheet:init() if caption then cols:addviews{ DataColumn{ - idx=#cols.subviews+1, label=caption, data_fn=function(unit) return (utils.binsearch(unit.status.current_soul.skills, i, 'id') or {rating=0}).rating @@ -286,7 +326,6 @@ function Spreadsheet:init() for _, wd in ipairs(df.global.plotinfo.labor_info.work_details) do cols:addviews{ ToggleColumn{ - idx=#cols.subviews+1, label=wd.name, data_fn=function(unit) return utils.binsearch(wd.assigned_units, unit.id) and true or false @@ -297,6 +336,31 @@ function Spreadsheet:init() } end + for _, workshop in ipairs(df.global.world.buildings.other.FURNACE_ANY) do + cols:addviews{ + ToggleColumn{ + label=workshop.name, + data_fn=function(unit) + return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false + end, + group='workshops', + shared=self.shared, + } + } + end + for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do + cols:addviews{ + ToggleColumn{ + label=workshop.name, + data_fn=function(unit) + return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false + end, + group='workshops', + shared=self.shared, + } + } + end + self:addviews{ widgets.TextButton{ view_id='left_group', @@ -319,7 +383,6 @@ function Spreadsheet:init() DataColumn{ view_id='name', frame={w=30}, - idx=0, label='Name', label_inset=8, data_fn=dfhack.units.getReadableName, @@ -329,6 +392,10 @@ function Spreadsheet:init() cols, } + -- set up initial sort: primary favorites, secondary name + self.shared.sort_stack[1] = {col=self.subviews.name, rev=false} + self.shared.sort_stack[2] = {col=self.subviews.favorites, rev=false} + self.list = self.subviews.name.subviews.col_list self:addviews{ widgets.Scrollbar{ @@ -342,13 +409,28 @@ function Spreadsheet:init() self:update_headers() end -function Spreadsheet:sort_by_current_row() +function Spreadsheet:sort_by_current_col() + -- TODO end function Spreadsheet:filter(search) + -- TODO end -function Spreadsheet:hide_current_row() +function Spreadsheet:zoom_to_prev_group() + -- TODO +end + +function Spreadsheet:zoom_to_next_group() + -- TODO +end + +function Spreadsheet:hide_current_col() + -- TODO +end + +function Spreadsheet:hide_current_col_group() + -- TODO end function Spreadsheet:jump_to_group(group) @@ -382,24 +464,18 @@ function Spreadsheet:get_visible_units(units) end function Spreadsheet:refresh() - self.shared.fault = false + local shared = self.shared + local cache = shared.cache + shared.fault = false self.subviews.name.dirty = true for _, col in ipairs(self.cols.subviews) do col.dirty = true end - local units = dfhack.units.getCitizens() - self.shared.cache.units = units - self.shared.cache.visible_units, self.shared.visible_unit_ids = self:get_visible_units(units) - local sort_idx = self.shared.sort_idx - self.shared.sort_rev = not self.shared.sort_rev - if sort_idx == -1 then - self.subviews.name:sort() - self.subviews.favorites:sort() - elseif sort_idx == 0 then - self.subviews.name:sort() - else - self.cols.subviews[sort_idx]:sort() - end + local units = self.get_units_fn() + cache.units = units + cache.visible_units, shared.filtered_unit_ids = self:get_visible_units(units) + shared.sort_order = utils.tabulate(function(i) return i end, 1, #shared.filtered_unit_ids) + shared.sort_stack[#shared.sort_stack].col:sort() self.dirty = false end @@ -582,7 +658,7 @@ function Manipulator:init() on_activate=function() self.subviews.sheet:zoom_to_next_group() end, }, widgets.HotkeyLabel{ - frame={b=1, l=54}, + frame={b=1, l=57}, auto_width=true, label='Hide', key='CUSTOM_CTRL_H', From 8386b22bb8661bd54aa55d601923b41650a1f037 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 00:27:51 -0700 Subject: [PATCH 163/811] actually get sorting working --- gui/manipulator.lua | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index ba4d33f25e..fc97fd09d3 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -132,9 +132,6 @@ function Column:init() end function Column:sort(make_primary) - if self.dirty then - self:refresh() - end local sort_stack = self.shared.sort_stack if make_primary then -- we are newly sorting by this column: reverse sort if we're already on top of the @@ -152,6 +149,12 @@ function Column:sort(make_primary) table.insert(sort_stack, {col=self, rev=false}) end end + for _,sort_spec in ipairs(sort_stack) do + local col = sort_spec.col + if col.dirty then + col:refresh() + end + end local compare = function(a, b) for idx=#sort_stack,1,-1 do local sort_spec = sort_stack[idx] @@ -171,8 +174,9 @@ function Column:sort(make_primary) end return 0 end + local order = utils.tabulate(function(i) return i end, 1, #self.shared.filtered_unit_ids) local spec = {compare=compare} - self.shared.sort_order = utils.make_sort_order(self.shared.sort_order, {spec}) + self.shared.sort_order = utils.make_sort_order(order, {spec}) end function Column:get_units() @@ -198,13 +202,16 @@ function Column:refresh() local data = self.data_fn(unit) local val = self.count_fn(data) if unit.id == self.shared.filtered_unit_ids[next_id_idx] then + local idx = next_id_idx table.insert(col_data, data) table.insert(choices, { - { - text=function() - local ordered_data = col_data[self.shared.sort_order[next_id_idx]] - return (not ordered_data or ordered_data == 0) and '-' or tostring(ordered_data) - end, + text={ + { + text=function() + local ordered_data = col_data[self.shared.sort_order[idx]] + return (not ordered_data or ordered_data == 0) and '-' or tostring(ordered_data) + end, + }, }, }) current = current + val @@ -276,6 +283,18 @@ Spreadsheet.ATTRS{ get_units_fn=DEFAULT_NIL, } +local function get_workshop_label(workshop, type_enum, bld_defs) + if #workshop.name > 0 then + return workshop.name + end + local type_name = type_enum[workshop.type] + if type_name == 'Custom' then + local bld_def = bld_defs[workshop.custom_type] + if bld_def then return bld_def.code end + end + return type_name +end + function Spreadsheet:init() self.left_col = 1 self.dirty = true @@ -339,7 +358,7 @@ function Spreadsheet:init() for _, workshop in ipairs(df.global.world.buildings.other.FURNACE_ANY) do cols:addviews{ ToggleColumn{ - label=workshop.name, + label=get_workshop_label(workshop, df.furnace_type, df.global.world.raws.buildings.furnaces), data_fn=function(unit) return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false end, @@ -351,7 +370,7 @@ function Spreadsheet:init() for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do cols:addviews{ ToggleColumn{ - label=workshop.name, + label=get_workshop_label(workshop, df.workshop_type, df.global.world.raws.buildings.workshops), data_fn=function(unit) return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false end, @@ -474,7 +493,6 @@ function Spreadsheet:refresh() local units = self.get_units_fn() cache.units = units cache.visible_units, shared.filtered_unit_ids = self:get_visible_units(units) - shared.sort_order = utils.tabulate(function(i) return i end, 1, #shared.filtered_unit_ids) shared.sort_stack[#shared.sort_stack].col:sort() self.dirty = false end @@ -671,7 +689,7 @@ function Manipulator:init() return self.needs_refresh and 'Refresh (unit list has changed)' or 'Refresh' end, text_pen=function() - return self.needs_refresh and COLOR_LIGHTRED or nil + return self.needs_refresh and COLOR_LIGHTRED or COLOR_GRAY end, key='CUSTOM_SHIFT_R', on_activate=function() From f570c74fc2ed0c5c6547aee78f457c24dca0973d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 00:51:25 -0700 Subject: [PATCH 164/811] fix rendering and sort order issues --- gui/manipulator.lua | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index fc97fd09d3..8e0b010ff9 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -115,10 +115,12 @@ function Column:init() widgets.Label{ view_id='col_current', frame={t=7, l=1+self.label_inset, w=4}, + auto_height=false, }, widgets.Label{ view_id='col_total', frame={t=8, l=1+self.label_inset, w=4}, + auto_height=false, }, widgets.List{ view_id='col_list', @@ -257,6 +259,12 @@ function DataColumn:init() return 1 end end + if not self.cmp_fn then + self.cmp_fn = function(a, b) + if type(a) == 'number' then return -utils.compare(a, b) end + return utils.compare(a, b) + end + end end ------------------------ @@ -473,13 +481,13 @@ function Spreadsheet:update_headers() end -- TODO: apply search and filtering -function Spreadsheet:get_visible_units(units) - local visible_units, visible_unit_ids = {}, {} +function Spreadsheet:filter_units(units) + local unit_ids, filtered_unit_ids = {}, {} for _, unit in ipairs(units) do - table.insert(visible_units, unit) - table.insert(visible_unit_ids, unit.id) + table.insert(unit_ids, unit.id) + table.insert(filtered_unit_ids, unit.id) end - return visible_units, visible_unit_ids + return unit_ids, filtered_unit_ids end function Spreadsheet:refresh() @@ -492,7 +500,7 @@ function Spreadsheet:refresh() end local units = self.get_units_fn() cache.units = units - cache.visible_units, shared.filtered_unit_ids = self:get_visible_units(units) + shared.unit_ids, shared.filtered_unit_ids = self:filter_units(units) shared.sort_stack[#shared.sort_stack].col:sort() self.dirty = false end From d846f4ac3f9fad02438d02a5f7345a619cea584d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 02:24:16 -0700 Subject: [PATCH 165/811] render toggle buttons for toggle columns and format stress so it fits in 4 characters --- gui/manipulator.lua | 153 +++++++++++++++++++++++++++++++------------- 1 file changed, 109 insertions(+), 44 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 8e0b010ff9..2ca8d0ffe3 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -4,6 +4,7 @@ local gui = require("gui") local json = require('json') local overlay = require('plugins.overlay') local presets = reqscript('internal/manipulator/presets') +local textures = require('gui.textures') local utils = require('utils') local widgets = require("gui.widgets") @@ -73,7 +74,6 @@ end Column = defclass(Column, widgets.Panel) Column.ATTRS{ - idx=DEFAULT_NIL, label=DEFAULT_NIL, group='', label_inset=0, @@ -83,6 +83,8 @@ Column.ATTRS{ data_fn=DEFAULT_NIL, count_fn=DEFAULT_NIL, cmp_fn=DEFAULT_NIL, + choice_fn=DEFAULT_NIL, + on_select=DEFAULT_NIL, } function Column:init() @@ -125,6 +127,7 @@ function Column:init() widgets.List{ view_id='col_list', frame={t=10, l=0, w=self.data_width}, + on_select=self.on_select, }, } @@ -206,16 +209,7 @@ function Column:refresh() if unit.id == self.shared.filtered_unit_ids[next_id_idx] then local idx = next_id_idx table.insert(col_data, data) - table.insert(choices, { - text={ - { - text=function() - local ordered_data = col_data[self.shared.sort_order[idx]] - return (not ordered_data or ordered_data == 0) and '-' or tostring(ordered_data) - end, - }, - }, - }) + table.insert(choices, self.choice_fn(function() return col_data[self.shared.sort_order[idx]] end)) current = current + val next_id_idx = next_id_idx + 1 end @@ -247,41 +241,91 @@ end -- DataColumn -- +local function data_cmp(a, b) + if type(a) == 'number' then return -utils.compare(a, b) end + return utils.compare(a, b) +end + +local function data_count(data) + if not data then return 0 end + if type(data) == 'number' then return data > 0 and 1 or 0 end + return 1 +end + +local function data_choice(get_ordered_data_fn) + return { + text={ + { + text=function() + local ordered_data = get_ordered_data_fn() + return (not ordered_data or ordered_data == 0) and '-' or tostring(ordered_data) + end, + }, + }, + } +end + DataColumn = defclass(DataColumn, Column) DataColumn.ATTRS{ + cmp_fn=data_cmp, + count_fn=data_count, + choice_fn=data_choice, } -function DataColumn:init() - if not self.count_fn then - self.count_fn = function(data) - if not data then return 0 end - if type(data) == 'number' then return data > 0 and 1 or 0 end - return 1 - end - end - if not self.cmp_fn then - self.cmp_fn = function(a, b) - if type(a) == 'number' then return -utils.compare(a, b) end - return utils.compare(a, b) - end - end -end - ------------------------ -- ToggleColumn -- +local ENABLED_PEN_LEFT = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} +local ENABLED_PEN_CENTER = dfhack.pen.parse{fg=COLOR_LIGHTGREEN, + tile=curry(textures.tp_control_panel, 2) or nil, ch=251} -- check +local ENABLED_PEN_RIGHT = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 3) or nil, ch=string.byte(']')} +local DISABLED_PEN_LEFT = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 4) or nil, ch=string.byte('[')} +local DISABLED_PEN_CENTER = dfhack.pen.parse{fg=COLOR_RED, + tile=curry(textures.tp_control_panel, 5) or nil, ch=string.byte('x')} +local DISABLED_PEN_RIGHT = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 6) or nil, ch=string.byte(']')} + +local function toggle_count(data) + return data and 1 or 0 +end + +local function toggle_choice(get_ordered_data_fn) + local function get_enabled_button_token(enabled_tile, disabled_tile) + return { + tile=function() return get_ordered_data_fn() and enabled_tile or disabled_tile end, + } + end + return { + text={ + get_enabled_button_token(ENABLED_PEN_LEFT, DISABLED_PEN_LEFT), + get_enabled_button_token(ENABLED_PEN_CENTER, DISABLED_PEN_CENTER), + get_enabled_button_token(ENABLED_PEN_RIGHT, DISABLED_PEN_RIGHT), + }, + } +end + ToggleColumn = defclass(ToggleColumn, Column) ToggleColumn.ATTRS{ - on_toggle=DEFAULT_NIL, + count_fn=toggle_count, + choice_fn=toggle_choice, } -function ToggleColumn:init() - if not self.count_fn then - self.count_fn = function(data) return data and 1 or 0 end - end +------------------------ +-- TagColumn +-- + +local function tag_select(_, choice) end +TagColumn = defclass(TagColumn, ToggleColumn) +TagColumn.ATTRS{ + on_select=tag_select, +} + ------------------------ -- Spreadsheet -- @@ -321,16 +365,37 @@ function Spreadsheet:init() cols:addviews{ ToggleColumn{ view_id='favorites', - label='Favorites', - data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, group='tags', + label='Favorites', shared=self.shared, + data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, }, DataColumn{ - label='Stress', - data_fn=function(unit) return unit.status.current_soul.personality.stress end, group='summary', + label='Stress', shared=self.shared, + data_fn=function(unit) return unit.status.current_soul.personality.stress end, + choice_fn=function(get_ordered_data_fn) + return { + text={ + { + text=function() + local ordered_data = get_ordered_data_fn() + if ordered_data > 99999 then + return '>99k' + elseif ordered_data > 9999 then + return ('%3dk'):format(ordered_data // 1000) + elseif ordered_data < -99999 then + return ' -' .. string.char(236) -- -∞ + elseif ordered_data < -999 then + return ('%3dk'):format(-(-ordered_data // 1000)) + end + return tostring(ordered_data) + end, + }, + }, + } + end, } } @@ -339,12 +404,12 @@ function Spreadsheet:init() if caption then cols:addviews{ DataColumn{ + group='skills', label=caption, + shared=self.shared, data_fn=function(unit) return (utils.binsearch(unit.status.current_soul.skills, i, 'id') or {rating=0}).rating end, - group='skills', - shared=self.shared, } } end @@ -353,12 +418,12 @@ function Spreadsheet:init() for _, wd in ipairs(df.global.plotinfo.labor_info.work_details) do cols:addviews{ ToggleColumn{ + group='work details', label=wd.name, + shared=self.shared, data_fn=function(unit) return utils.binsearch(wd.assigned_units, unit.id) and true or false end, - group='work details', - shared=self.shared, } } end @@ -366,24 +431,24 @@ function Spreadsheet:init() for _, workshop in ipairs(df.global.world.buildings.other.FURNACE_ANY) do cols:addviews{ ToggleColumn{ + group='workshops', label=get_workshop_label(workshop, df.furnace_type, df.global.world.raws.buildings.furnaces), + shared=self.shared, data_fn=function(unit) return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false end, - group='workshops', - shared=self.shared, } } end for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do cols:addviews{ ToggleColumn{ + group='workshops', label=get_workshop_label(workshop, df.workshop_type, df.global.world.raws.buildings.workshops), + shared=self.shared, data_fn=function(unit) return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false end, - group='workshops', - shared=self.shared, } } end From 1af885788e3bb44741904efb1a5b5e05a279e3d3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 17:06:07 -0700 Subject: [PATCH 166/811] implement favorites toggling and stress colors --- gui/manipulator.lua | 61 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 46 insertions(+), 15 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 2ca8d0ffe3..bad0a64cd5 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -84,7 +84,6 @@ Column.ATTRS{ count_fn=DEFAULT_NIL, cmp_fn=DEFAULT_NIL, choice_fn=DEFAULT_NIL, - on_select=DEFAULT_NIL, } function Column:init() @@ -127,7 +126,7 @@ function Column:init() widgets.List{ view_id='col_list', frame={t=10, l=0, w=self.data_width}, - on_select=self.on_select, + on_submit=self:callback('on_select'), }, } @@ -136,6 +135,10 @@ function Column:init() self.dirty = true end +-- overridden by subclasses +function Column:on_select(idx, choice) +end + function Column:sort(make_primary) local sort_stack = self.shared.sort_stack if make_primary then @@ -199,6 +202,14 @@ function Column:get_units() return units end +function Column:get_sorted_unit_id(idx) + return self.shared.filtered_unit_ids[self.shared.sort_order[idx]] +end + +function Column:get_sorted_data(idx) + return self.col_data[self.shared.sort_order[idx]] +end + function Column:refresh() local col_data, choices = {}, {} local current, total = 0, 0 @@ -209,7 +220,7 @@ function Column:refresh() if unit.id == self.shared.filtered_unit_ids[next_id_idx] then local idx = next_id_idx table.insert(col_data, data) - table.insert(choices, self.choice_fn(function() return col_data[self.shared.sort_order[idx]] end)) + table.insert(choices, self.choice_fn(function() return self:get_sorted_data(idx) end)) current = current + val next_id_idx = next_id_idx + 1 end @@ -312,20 +323,18 @@ ToggleColumn = defclass(ToggleColumn, Column) ToggleColumn.ATTRS{ count_fn=toggle_count, choice_fn=toggle_choice, + toggle_fn=DEFAULT_NIL, } ------------------------- --- TagColumn --- - -local function tag_select(_, choice) +function ToggleColumn:on_select(idx, choice) + if not self.toggle_fn then return end + local unit_id = self:get_sorted_unit_id(idx) + local prev_val = self:get_sorted_data(idx) + print(idx, unit_id, dfhack.units.getReadableName(df.unit.find(unit_id)), prev_val) + self.toggle_fn(unit_id, prev_val) + self.dirty = true end -TagColumn = defclass(TagColumn, ToggleColumn) -TagColumn.ATTRS{ - on_select=tag_select, -} - ------------------------ -- Spreadsheet -- @@ -368,7 +377,16 @@ function Spreadsheet:init() group='tags', label='Favorites', shared=self.shared, - data_fn=function(unit) return utils.binsearch(ensure_key(config.data, 'favorites'), unit.id) end, + data_fn=function(unit) return utils.binsearch(ensure_key(state, 'favorites'), unit.id) and true or false end, + toggle_fn=function(unit_id, prev_val) + local fav_vec = ensure_key(state, 'favorites') + if prev_val then + utils.erase_sorted(fav_vec, unit_id) + else + utils.insert_sorted(fav_vec, unit_id) + end + persist_state() + end, }, DataColumn{ group='summary', @@ -390,7 +408,20 @@ function Spreadsheet:init() elseif ordered_data < -999 then return ('%3dk'):format(-(-ordered_data // 1000)) end - return tostring(ordered_data) + return ('%4d'):format(ordered_data) + end, + pen=function() + local ordered_data = get_ordered_data_fn() + local level = dfhack.units.getStressCategoryRaw(ordered_data) + local is_graphics = dfhack.screen.inGraphicsMode() + -- match colors of stress faces depending on mode + if level == 0 then return COLOR_RED end + if level == 1 then return COLOR_LIGHTRED end + if level == 2 then return is_graphics and COLOR_BROWN or COLOR_YELLOW end + if level == 3 then return is_graphics and COLOR_YELLOW or COLOR_WHITE end + if level == 4 then return is_graphics and COLOR_CYAN or COLOR_GREEN end + if level == 5 then return is_graphics and COLOR_GREEN or COLOR_LIGHTGREEN end + return is_graphics and COLOR_LIGHTGREEN or COLOR_LIGHTCYAN end, }, }, From beef160837aa2c14af037334cfaf7d3cb6e187fd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 17:30:55 -0700 Subject: [PATCH 167/811] implement toggling for workshops and work details --- gui/manipulator.lua | 57 ++++++++++++++++++++++++++------------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index bad0a64cd5..81c5b2c369 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -330,7 +330,6 @@ function ToggleColumn:on_select(idx, choice) if not self.toggle_fn then return end local unit_id = self:get_sorted_unit_id(idx) local prev_val = self:get_sorted_data(idx) - print(idx, unit_id, dfhack.units.getReadableName(df.unit.find(unit_id)), prev_val) self.toggle_fn(unit_id, prev_val) self.dirty = true end @@ -446,7 +445,8 @@ function Spreadsheet:init() end end - for _, wd in ipairs(df.global.plotinfo.labor_info.work_details) do + local work_details = df.global.plotinfo.labor_info.work_details + for _, wd in ipairs(work_details) do cols:addviews{ ToggleColumn{ group='work details', @@ -455,34 +455,43 @@ function Spreadsheet:init() data_fn=function(unit) return utils.binsearch(wd.assigned_units, unit.id) and true or false end, - } - } - end - - for _, workshop in ipairs(df.global.world.buildings.other.FURNACE_ANY) do - cols:addviews{ - ToggleColumn{ - group='workshops', - label=get_workshop_label(workshop, df.furnace_type, df.global.world.raws.buildings.furnaces), - shared=self.shared, - data_fn=function(unit) - return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false + toggle_fn=function(unit_id, prev_val) + -- TODO: poke DF to actually apply the work details to units + if prev_val then + utils.erase_sorted(wd.assigned_units, unit_id) + else + utils.insert_sorted(wd.assigned_units, unit_id) + end end, } } end - for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do - cols:addviews{ - ToggleColumn{ - group='workshops', - label=get_workshop_label(workshop, df.workshop_type, df.global.world.raws.buildings.workshops), - shared=self.shared, - data_fn=function(unit) - return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false - end, + + local function add_workshops(vec, type_enum, type_defs) + for _, workshop in ipairs(vec) do + cols:addviews{ + ToggleColumn{ + group='workshops', + label=get_workshop_label(workshop, type_enum, type_defs), + shared=self.shared, + data_fn=function(unit) + return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false + end, + toggle_fn=function(unit_id, prev_val) + if prev_val then + utils.erase_sorted(workshop.profile.permitted_workers, unit_id) + else + -- there can be only one + workshop.profile.permitted_workers:resize(0) + workshop.profile.permitted_workers:insert('#', unit_id) + end + end, + } } - } + end end + add_workshops(df.global.world.buildings.other.FURNACE_ANY, df.furnace_type, df.global.world.raws.buildings.furnaces) + add_workshops(df.global.world.buildings.other.WORKSHOP_ANY, df.workshop_type, df.global.world.raws.buildings.workshops) self:addviews{ widgets.TextButton{ From b77babfdfaee73c204cb073127306647a277695b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 17:34:55 -0700 Subject: [PATCH 168/811] add skeleton docs --- docs/gui/manipulator.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/gui/manipulator.rst diff --git a/docs/gui/manipulator.rst b/docs/gui/manipulator.rst new file mode 100644 index 0000000000..0cdbd8990e --- /dev/null +++ b/docs/gui/manipulator.rst @@ -0,0 +1,15 @@ +gui/manipulator +=============== + +.. dfhack-tool:: + :summary: Multi-function unit management interface. + :tags: fort productivity units + +This spreadsheet-like UI allows you to see all your units, their skills, properties, assignments, etc. at a glance. You can search, sort, and filter to see exactly the information that you're looking for. Moreover, you can assign units to burrows, squads, work details, and workshops. + +Usage +----- + +:: + + gui/manipulator From cc79310063a2fca2aaeae875fd509ee3b6ee5168 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 18:00:36 -0700 Subject: [PATCH 169/811] fix list width, factor out sorted vector logic --- gui/manipulator.lua | 43 ++++++++++++++++++++----------------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 81c5b2c369..3cff2d1812 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -125,7 +125,7 @@ function Column:init() }, widgets.List{ view_id='col_list', - frame={t=10, l=0, w=self.data_width}, + frame={t=10, l=0, w=self.data_width+2}, -- +2 for the invisible scrollbar on_submit=self:callback('on_select'), }, } @@ -319,6 +319,18 @@ local function toggle_choice(get_ordered_data_fn) } end +local function toggle_sorted_vec_data(vec, unit) + return utils.binsearch(vec, unit.id) and true or false +end + +local function toggle_sorted_vec(vec, unit_id, prev_val) + if prev_val then + utils.erase_sorted(vec, unit_id) + else + utils.insert_sorted(vec, unit_id) + end +end + ToggleColumn = defclass(ToggleColumn, Column) ToggleColumn.ATTRS{ count_fn=toggle_count, @@ -376,14 +388,9 @@ function Spreadsheet:init() group='tags', label='Favorites', shared=self.shared, - data_fn=function(unit) return utils.binsearch(ensure_key(state, 'favorites'), unit.id) and true or false end, + data_fn=curry(toggle_sorted_vec_data, state.favorites), toggle_fn=function(unit_id, prev_val) - local fav_vec = ensure_key(state, 'favorites') - if prev_val then - utils.erase_sorted(fav_vec, unit_id) - else - utils.insert_sorted(fav_vec, unit_id) - end + toggle_sorted_vec(state.favorites, unit_id, prev_val) persist_state() end, }, @@ -452,16 +459,10 @@ function Spreadsheet:init() group='work details', label=wd.name, shared=self.shared, - data_fn=function(unit) - return utils.binsearch(wd.assigned_units, unit.id) and true or false - end, + data_fn=curry(toggle_sorted_vec_data, wd.assigned_units), toggle_fn=function(unit_id, prev_val) + toggle_sorted_vec(wd.assigned_units, unit_id, prev_val) -- TODO: poke DF to actually apply the work details to units - if prev_val then - utils.erase_sorted(wd.assigned_units, unit_id) - else - utils.insert_sorted(wd.assigned_units, unit_id) - end end, } } @@ -474,17 +475,13 @@ function Spreadsheet:init() group='workshops', label=get_workshop_label(workshop, type_enum, type_defs), shared=self.shared, - data_fn=function(unit) - return utils.binsearch(workshop.profile.permitted_workers, unit.id) and true or false - end, + data_fn=curry(toggle_sorted_vec_data, workshop.profile.permitted_workers), toggle_fn=function(unit_id, prev_val) - if prev_val then - utils.erase_sorted(workshop.profile.permitted_workers, unit_id) - else + if not prev_val then -- there can be only one workshop.profile.permitted_workers:resize(0) - workshop.profile.permitted_workers:insert('#', unit_id) end + toggle_sorted_vec(workshop.profile.permitted_workers, unit_id, prev_val) end, } } From 87507e0cb191cf2d3f8c58e224f2b7b6fcf6af8e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 18:47:36 -0700 Subject: [PATCH 170/811] implement name search --- gui/manipulator.lua | 49 ++++++++++++++++++++++++--------------------- 1 file changed, 26 insertions(+), 23 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 3cff2d1812..5fc7d2fd6a 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -369,6 +369,7 @@ end function Spreadsheet:init() self.left_col = 1 + self.prev_filter = '' self.dirty = true self.shared = { @@ -542,10 +543,6 @@ function Spreadsheet:sort_by_current_col() -- TODO end -function Spreadsheet:filter(search) - -- TODO -end - function Spreadsheet:zoom_to_prev_group() -- TODO end @@ -582,27 +579,34 @@ function Spreadsheet:update_headers() end end --- TODO: apply search and filtering -function Spreadsheet:filter_units(units) - local unit_ids, filtered_unit_ids = {}, {} - for _, unit in ipairs(units) do - table.insert(unit_ids, unit.id) - table.insert(filtered_unit_ids, unit.id) - end - return unit_ids, filtered_unit_ids -end - -function Spreadsheet:refresh() +-- TODO: support column addressing for searching/filtering (e.g. "skills/Armoring:>10") +function Spreadsheet:refresh(filter, full_refresh) local shared = self.shared - local cache = shared.cache shared.fault = false self.subviews.name.dirty = true for _, col in ipairs(self.cols.subviews) do col.dirty = true end - local units = self.get_units_fn() - cache.units = units - shared.unit_ids, shared.filtered_unit_ids = self:filter_units(units) + local incremental = not full_refresh and self.prev_filter and filter:startswith(self.prev_filter) + if not incremental then + local units = self.get_units_fn() + shared.cache.units = units + shared.unit_ids = utils.tabulate(function(idx) return units[idx].id end, 1, #units) + end + shared.filtered_unit_ids = copyall(shared.unit_ids) + if #filter > 0 then + local col = self.subviews.name + col:refresh() + for idx=#col.col_data,1,-1 do + local data = col.col_data[idx] + if (not utils.search_text(data, filter)) then + table.remove(shared.filtered_unit_ids, idx) + end + end + if #col.col_data ~= #shared.filtered_unit_ids then + col.dirty = true + end + end shared.sort_stack[#shared.sort_stack].col:sort() self.dirty = false end @@ -652,7 +656,7 @@ end function Spreadsheet:render(dc) if self.dirty or self.shared.fault then - self:refresh() + self:refresh(self.prev_filter, true) self:updateLayout() end local page_top = self.list.page_top @@ -723,7 +727,7 @@ function Manipulator:init() frame={l=0, t=0}, key='FILTER', label_text='Search: ', - on_change=function(val) self.subviews.sheet:filter(val) end, + on_change=function(text) self.subviews.sheet:refresh(text, false) end, }, widgets.Divider{ frame={l=0, r=0, t=2, h=1}, @@ -803,8 +807,7 @@ function Manipulator:init() end, key='CUSTOM_SHIFT_R', on_activate=function() - self.subviews.sheet:refresh() - self.subviews.sheet:filter(self.subviews.search.text) + self.subviews.sheet:refresh(self.subviews.search.text, true) end, }, }, From 529fd42e48e2272846c7e590a6cab354e07d1f67 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 19:34:09 -0700 Subject: [PATCH 171/811] synchronize list row selection and add sort indicator --- gui/manipulator.lua | 63 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 8 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 5fc7d2fd6a..98c2fab506 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -86,6 +86,10 @@ Column.ATTRS{ choice_fn=DEFAULT_NIL, } +local CH_DOT = string.char(15) +local CH_UP = string.char(30) +local CH_DN = string.char(31) + function Column:init() self.frame = utils.assign({t=0, b=0, l=0, w=14}, self.frame or {}) @@ -105,11 +109,43 @@ function Column:init() frame_style=gui.FRAME_INTERIOR, frame_style_b=false, }, - widgets.HotkeyLabel{ + widgets.Panel{ view_id='col_label', frame={l=self.label_inset, t=4}, - label=self.label, - on_activate=self:callback('sort', true), + subviews={ + widgets.HotkeyLabel{ + frame={l=0, t=0, w=1}, + label=CH_DN, + text_pen=COLOR_LIGHTGREEN, + visible=function() + local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] + return sort_spec.col == self and not sort_spec.rev + end, + }, + widgets.HotkeyLabel{ + frame={l=0, t=0, w=1}, + label=CH_UP, + text_pen=COLOR_LIGHTGREEN, + visible=function() + local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] + return sort_spec.col == self and sort_spec.rev + end, + }, + widgets.HotkeyLabel{ + frame={l=0, t=0, w=1}, + label=CH_DOT, + text_pen=COLOR_GRAY, + visible=function() + local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] + return sort_spec.col ~= self + end, + }, + widgets.HotkeyLabel{ + frame={l=1, t=0}, + label=self.label, + on_activate=self:callback('sort', true), + }, + }, }, }, }, @@ -135,8 +171,14 @@ function Column:init() self.dirty = true end --- overridden by subclasses +-- extended by subclasses function Column:on_select(idx, choice) + -- conveniently, this will be nil for the namelist column itself, + -- avoiding an infinite loop + local namelist = self.parent_view.parent_view.namelist + if namelist then + namelist:setSelected(idx) + end end function Column:sort(make_primary) @@ -339,6 +381,7 @@ ToggleColumn.ATTRS{ } function ToggleColumn:on_select(idx, choice) + ToggleColumn.super.on_select(self, idx, choice) if not self.toggle_fn then return end local unit_id = self:get_sorted_unit_id(idx) local prev_val = self:get_sorted_data(idx) @@ -526,15 +569,16 @@ function Spreadsheet:init() self.shared.sort_stack[1] = {col=self.subviews.name, rev=false} self.shared.sort_stack[2] = {col=self.subviews.favorites, rev=false} - self.list = self.subviews.name.subviews.col_list + self.namelist = self.subviews.name.subviews.col_list self:addviews{ widgets.Scrollbar{ view_id='scrollbar', frame={t=7, r=0}, - on_scroll=self.list:callback('on_scrollbar'), + on_scroll=self.namelist:callback('on_scrollbar'), } } - self.list.scrollbar = self.subviews.scrollbar + self.namelist.scrollbar = self.subviews.scrollbar + self.namelist:setFocus(true) self:update_headers() end @@ -659,9 +703,11 @@ function Spreadsheet:render(dc) self:refresh(self.prev_filter, true) self:updateLayout() end - local page_top = self.list.page_top + local page_top = self.namelist.page_top + local selected = self.namelist:getSelected() for _, col in ipairs(self.cols.subviews) do col.subviews.col_list.page_top = page_top + col.subviews.col_list:setSelected(selected) end Spreadsheet.super.render(self, dc) self.shared.cache = {} @@ -728,6 +774,7 @@ function Manipulator:init() key='FILTER', label_text='Search: ', on_change=function(text) self.subviews.sheet:refresh(text, false) end, + on_unfocus=function() self.subviews.sheet.namelist:setFocus(true) end, }, widgets.Divider{ frame={l=0, r=0, t=2, h=1}, From 3e81a8e2deda45aa921cbddb56f6c8b4410807ed Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 May 2024 21:04:33 -0700 Subject: [PATCH 172/811] implement hiding columns --- gui/manipulator.lua | 99 ++++++++++++++++++++++++++++++++++++++------- 1 file changed, 85 insertions(+), 14 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 98c2fab506..798b6e21d9 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -68,6 +68,24 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) end +------------------------ +-- ColumnMenu +-- + +ColumnMenu = defclass(ColumnMenu, widgets.Panel) +ColumnMenu.ATTRS{ + frame_style=gui.FRAME_INTERIOR, +} + +function ColumnMenu:init() +end + +function ColumnMenu:show() + self.prev_focus_owner = self.focus_group.cur + self.visible = true +end + + ------------------------ -- Column -- @@ -143,7 +161,7 @@ function Column:init() widgets.HotkeyLabel{ frame={l=1, t=0}, label=self.label, - on_activate=self:callback('sort', true), + on_activate=self:callback('on_click'), }, }, }, @@ -181,6 +199,23 @@ function Column:on_select(idx, choice) end end +function Column:on_click() + local modifiers = dfhack.internal.getModifiers() + if modifiers.shift then + for _,col in ipairs(self.parent_view.subviews) do + if col.group == self.group then + col.hidden = true + end + end + self.shared.refresh_headers = true + elseif modifiers.ctrl then + self.hidden = true + self.shared.refresh_headers = true + else + self:sort(true) + end +end + function Column:sort(make_primary) local sort_stack = self.shared.sort_stack if make_primary then @@ -413,7 +448,6 @@ end function Spreadsheet:init() self.left_col = 1 self.prev_filter = '' - self.dirty = true self.shared = { unit_ids={}, @@ -421,6 +455,8 @@ function Spreadsheet:init() sort_stack={}, sort_order={}, -- list of indices into filtered_unit_ids (or cache.filtered_units) cache={}, -- cached pointers; reset at end of frame + refresh_units=true, + refresh_headers=true, } local cols = widgets.Panel{} @@ -579,8 +615,6 @@ function Spreadsheet:init() } self.namelist.scrollbar = self.subviews.scrollbar self.namelist:setFocus(true) - - self:update_headers() end function Spreadsheet:sort_by_current_col() @@ -621,6 +655,7 @@ function Spreadsheet:update_headers() ord = ord + 1 end end + self.shared.refresh_headers = false end -- TODO: support column addressing for searching/filtering (e.g. "skills/Armoring:>10") @@ -652,7 +687,7 @@ function Spreadsheet:refresh(filter, full_refresh) end end shared.sort_stack[#shared.sort_stack].col:sort() - self.dirty = false + self.shared.refresh_units = false end function Spreadsheet:update_col_layout(idx, col, width, group, max_width) @@ -676,6 +711,7 @@ function Spreadsheet:preUpdateLayout(parent_rect) for idx, col in ipairs(self.cols.subviews) do local prev_group = group width, group = self:update_col_layout(idx, col, width, group, parent_rect.width) + if col.hidden then goto continue end if not next_col_group and group ~= '' and not col.visible and col.group ~= cur_col_group then next_col_group = col.group local str = next_col_group .. string.char(26) -- right arrow @@ -695,12 +731,19 @@ function Spreadsheet:preUpdateLayout(parent_rect) left_group.label.on_activate=self:callback('jump_to_group', prev_col_group) left_group.visible = true end + ::continue:: end + self.shared.layout_changed = false end function Spreadsheet:render(dc) - if self.dirty or self.shared.fault then - self:refresh(self.prev_filter, true) + if self.shared.refresh_headers or self.shared.refresh_units then + if self.shared.refresh_units then + self:refresh(self.prev_filter, true) + end + if self.shared.refresh_headers then + self:update_headers() + end self:updateLayout() end local page_top = self.namelist.page_top @@ -725,16 +768,44 @@ end function Spreadsheet:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then - self.left_col = math.max(1, self.left_col - 1) - self:updateLayout() + for idx=self.left_col-1,1,-1 do + if not self.cols.subviews[idx].hidden then + self.left_col = idx + self:updateLayout() + break + end + end elseif keys.KEYBOARD_CURSOR_LEFT_FAST then - self.left_col = math.max(1, self.left_col - self:get_num_visible_cols()) + local remaining = self:get_num_visible_cols() + for idx=self.left_col-1,1,-1 do + if not self.cols.subviews[idx].hidden then + remaining = remaining - 1 + self.left_col = idx + if remaining == 0 then + break + end + end + end self:updateLayout() elseif keys.KEYBOARD_CURSOR_RIGHT then - self.left_col = math.min(#self.cols.subviews, self.left_col + 1) - self:updateLayout() + for idx=self.left_col+1,#self.cols.subviews do + if not self.cols.subviews[idx].hidden then + self.left_col = idx + self:updateLayout() + break + end + end elseif keys.KEYBOARD_CURSOR_RIGHT_FAST then - self.left_col = math.min(#self.cols.subviews, self.left_col + self:get_num_visible_cols()) + local remaining = self:get_num_visible_cols() + for idx=self.left_col+1,#self.cols.subviews do + if not self.cols.subviews[idx].hidden then + remaining = remaining - 1 + self.left_col = idx + if remaining == 0 then + break + end + end + end self:updateLayout() end return Spreadsheet.super.onInput(self, keys) @@ -847,7 +918,7 @@ function Manipulator:init() frame={b=0, l=0}, auto_width=true, label=function() - return self.needs_refresh and 'Refresh (unit list has changed)' or 'Refresh' + return self.needs_refresh and 'Refresh units (new units have arrived)' or 'Refresh units' end, text_pen=function() return self.needs_refresh and COLOR_LIGHTRED or COLOR_GRAY From 13f8afe9b06d1dbb83451203035c2a977a8acbd6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 01:15:24 -0700 Subject: [PATCH 173/811] implement column popup menu --- gui/manipulator.lua | 195 +++++++++++++++++++++++++++++--------------- 1 file changed, 130 insertions(+), 65 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 798b6e21d9..a4534963e6 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -75,16 +75,62 @@ end ColumnMenu = defclass(ColumnMenu, widgets.Panel) ColumnMenu.ATTRS{ frame_style=gui.FRAME_INTERIOR, + frame_background=gui.CLEAR_PEN, + visible=false, + col=DEFAULT_NIL, } function ColumnMenu:init() + local choices = {} + + table.insert(choices, { + text='Sort', + fn=self.col:callback('sort', true), + }) + table.insert(choices, { + text='Hide column', + fn=self.col:callback('hide_column'), + }) + table.insert(choices, { + text='Hide group', + fn=self.col:callback('hide_group'), + }) + + self:addviews{ + widgets.List{ + choices=choices, + on_submit=function(_, choice) + choice.fn() + self:hide() + end, + }, + } end function ColumnMenu:show() self.prev_focus_owner = self.focus_group.cur self.visible = true + self:setFocus(true) end +function ColumnMenu:hide() + self.visible = false + if self.prev_focus_owner then + self.prev_focus_owner:setFocus(true) + end +end + +function ColumnMenu:onInput(keys) + if ColumnMenu.super.onInput(self, keys) then + return true + end + if keys._MOUSE_R then + self:hide() + elseif keys._MOUSE_L and not self:getMouseFramePos() then + self:hide() + end + return true +end ------------------------ -- Column @@ -111,6 +157,10 @@ local CH_DN = string.char(31) function Column:init() self.frame = utils.assign({t=0, b=0, l=0, w=14}, self.frame or {}) + local function show_menu() + self.subviews.col_menu:show() + end + self:addviews{ widgets.TextButton{ view_id='col_group', @@ -118,8 +168,23 @@ function Column:init() label=self.group, visible=#self.group > 0, }, + widgets.Label{ + view_id='col_current', + frame={t=7, l=1+self.label_inset, w=4}, + auto_height=false, + }, + widgets.Label{ + view_id='col_total', + frame={t=8, l=1+self.label_inset, w=4}, + auto_height=false, + }, + widgets.List{ + view_id='col_list', + frame={t=10, l=0, w=self.data_width+2}, -- +2 for the invisible scrollbar + on_submit=self:callback('on_select'), + }, widgets.Panel{ - frame={t=2, l=0, h=5}, + frame={t=2, l=0, h=10}, subviews={ widgets.Divider{ view_id='col_stem', @@ -135,6 +200,7 @@ function Column:init() frame={l=0, t=0, w=1}, label=CH_DN, text_pen=COLOR_LIGHTGREEN, + on_activate=show_menu, visible=function() local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] return sort_spec.col == self and not sort_spec.rev @@ -144,6 +210,7 @@ function Column:init() frame={l=0, t=0, w=1}, label=CH_UP, text_pen=COLOR_LIGHTGREEN, + on_activate=show_menu, visible=function() local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] return sort_spec.col == self and sort_spec.rev @@ -153,6 +220,7 @@ function Column:init() frame={l=0, t=0, w=1}, label=CH_DOT, text_pen=COLOR_GRAY, + on_activate=show_menu, visible=function() local sort_spec = self.shared.sort_stack[#self.shared.sort_stack] return sort_spec.col ~= self @@ -163,25 +231,15 @@ function Column:init() label=self.label, on_activate=self:callback('on_click'), }, + ColumnMenu{ + view_id='col_menu', + frame={l=0, t=1, h=5}, + col=self, + }, }, }, }, }, - widgets.Label{ - view_id='col_current', - frame={t=7, l=1+self.label_inset, w=4}, - auto_height=false, - }, - widgets.Label{ - view_id='col_total', - frame={t=8, l=1+self.label_inset, w=4}, - auto_height=false, - }, - widgets.List{ - view_id='col_list', - frame={t=10, l=0, w=self.data_width+2}, -- +2 for the invisible scrollbar - on_submit=self:callback('on_select'), - }, } self.subviews.col_list.scrollbar.visible = false @@ -199,18 +257,26 @@ function Column:on_select(idx, choice) end end +function Column:hide_column() + self.hidden = true + self.shared.refresh_headers = true +end + +function Column:hide_group() + for _,col in ipairs(self.parent_view.subviews) do + if col.group == self.group then + col.hidden = true + end + end + self.shared.refresh_headers = true +end + function Column:on_click() local modifiers = dfhack.internal.getModifiers() if modifiers.shift then - for _,col in ipairs(self.parent_view.subviews) do - if col.group == self.group then - col.hidden = true - end - end - self.shared.refresh_headers = true + self:hide_group() elseif modifiers.ctrl then - self.hidden = true - self.shared.refresh_headers = true + self:hide_column() else self:sort(true) end @@ -424,6 +490,33 @@ function ToggleColumn:on_select(idx, choice) self.dirty = true end +------------------------ +-- Cols +-- + +Cols = defclass(Cols, widgets.Panel) + +function Cols:renderSubviews(dc) + -- allow labels of columns to the left to overwrite the stems of columns on the right + for idx=#self.subviews,1,-1 do + local child = self.subviews[idx] + if utils.getval(child.visible) then + child:render(dc) + end + end + -- but group labels and popup menus on the right should overwrite long group names on the left + for _,child in ipairs(self.subviews) do + if utils.getval(child.visible) then + if utils.getval(child.subviews.col_group.visible) then + child.subviews.col_group:render(dc) + end + if utils.getval(child.subviews.col_menu.visible) then + child:render(dc) + end + end + end +end + ------------------------ -- Spreadsheet -- @@ -459,7 +552,7 @@ function Spreadsheet:init() refresh_headers=true, } - local cols = widgets.Panel{} + local cols = Cols{} self.cols = cols cols:addviews{ @@ -574,11 +667,13 @@ function Spreadsheet:init() widgets.TextButton{ view_id='left_group', frame={t=1, l=0, h=1}, + key='CUSTOM_CTRL_Y', visible=false, }, widgets.TextButton{ view_id='right_group', frame={t=1, r=0, h=1}, + key='CUSTOM_CTRL_T', visible=false, }, widgets.Label{ @@ -621,14 +716,6 @@ function Spreadsheet:sort_by_current_col() -- TODO end -function Spreadsheet:zoom_to_prev_group() - -- TODO -end - -function Spreadsheet:zoom_to_next_group() - -- TODO -end - function Spreadsheet:hide_current_col() -- TODO end @@ -715,10 +802,10 @@ function Spreadsheet:preUpdateLayout(parent_rect) if not next_col_group and group ~= '' and not col.visible and col.group ~= cur_col_group then next_col_group = col.group local str = next_col_group .. string.char(26) -- right arrow - right_group:setLabel(str) - right_group.frame.w = #str + 2 + right_group.frame.w = #str + 10 right_group.label.on_activate=self:callback('jump_to_group', next_col_group) right_group.visible = true + right_group:setLabel(str) end if cur_col_group ~= col.group then prev_col_group = cur_col_group @@ -726,10 +813,10 @@ function Spreadsheet:preUpdateLayout(parent_rect) cur_col_group = col.group if prev_group == '' and group ~= '' and prev_col_group and prev_col_group ~= '' then local str = string.char(27) .. prev_col_group -- left arrow - left_group:setLabel(str) - left_group.frame.w = #str + 2 + left_group.frame.w = #str + 10 left_group.label.on_activate=self:callback('jump_to_group', prev_col_group) left_group.visible = true + left_group:setLabel(str) end ::continue:: end @@ -869,48 +956,26 @@ function Manipulator:init() subviews={ widgets.WrappedLabel{ frame={t=0, l=0}, - text_to_wrap='Use arrow keys or middle click drag to navigate cells. Left click or ENTER to toggle current cell.', - }, - widgets.Label{ - frame={b=2, l=0}, - text='Current column:', + text_to_wrap='Use arrow keys or middle click drag to navigate cells. Left click or ENTER to toggle cell.', }, widgets.HotkeyLabel{ - frame={b=2, l=17}, + frame={b=2, l=0}, auto_width=true, label='Sort/reverse sort', key='CUSTOM_SHIFT_S', on_activate=function() self.subviews.sheet:sort_by_current_col() end, }, widgets.HotkeyLabel{ - frame={b=2, l=39}, + frame={b=2, l=22}, auto_width=true, - label='Hide', + label='Hide column', key='CUSTOM_SHIFT_H', on_activate=function() self.subviews.sheet:hide_current_col() end, }, - widgets.Label{ - frame={b=1, l=0}, - text='Current group:', - }, - widgets.HotkeyLabel{ - frame={b=1, l=17}, - auto_width=true, - label='Prev group', - key='CUSTOM_CTRL_Y', - on_activate=function() self.subviews.sheet:zoom_to_prev_group() end, - }, - widgets.HotkeyLabel{ - frame={b=1, l=37}, - auto_width=true, - label='Next group', - key='CUSTOM_CTRL_T', - on_activate=function() self.subviews.sheet:zoom_to_next_group() end, - }, widgets.HotkeyLabel{ - frame={b=1, l=57}, + frame={b=2, l=38}, auto_width=true, - label='Hide', + label='Hide group', key='CUSTOM_CTRL_H', on_activate=function() self.subviews.sheet:hide_current_col_group() end, }, From 7232ba845f8143c363e6a05401abafa3dc6d3cad Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 02:11:28 -0700 Subject: [PATCH 174/811] popup menus for jump to col and unhide col --- gui/manipulator.lua | 164 +++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 154 insertions(+), 10 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index a4534963e6..9b233dae99 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -262,6 +262,11 @@ function Column:hide_column() self.shared.refresh_headers = true end +function Column:unhide_column() + self.hidden = false + self.shared.refresh_headers = true +end + function Column:hide_group() for _,col in ipairs(self.parent_view.subviews) do if col.group == self.group then @@ -727,10 +732,14 @@ end function Spreadsheet:jump_to_group(group) for i, col in ipairs(self.cols.subviews) do if not col.hidden and col.group == group then - self.left_col = i + self:jump_to_col(i) break end end +end + +function Spreadsheet:jump_to_col(idx) + self.left_col = idx self:updateLayout() end @@ -857,47 +866,133 @@ function Spreadsheet:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then for idx=self.left_col-1,1,-1 do if not self.cols.subviews[idx].hidden then - self.left_col = idx - self:updateLayout() + self:jump_to_col(idx) break end end elseif keys.KEYBOARD_CURSOR_LEFT_FAST then local remaining = self:get_num_visible_cols() + local target_col = self.left_col for idx=self.left_col-1,1,-1 do if not self.cols.subviews[idx].hidden then remaining = remaining - 1 - self.left_col = idx + target_col = idx if remaining == 0 then break end end end - self:updateLayout() + self:jump_to_col(target_col) elseif keys.KEYBOARD_CURSOR_RIGHT then for idx=self.left_col+1,#self.cols.subviews do if not self.cols.subviews[idx].hidden then - self.left_col = idx - self:updateLayout() + self:jump_to_col(idx) break end end elseif keys.KEYBOARD_CURSOR_RIGHT_FAST then local remaining = self:get_num_visible_cols() + local target_col = self.left_col for idx=self.left_col+1,#self.cols.subviews do if not self.cols.subviews[idx].hidden then remaining = remaining - 1 - self.left_col = idx + target_col = idx if remaining == 0 then break end end end - self:updateLayout() + self:jump_to_col(target_col) end return Spreadsheet.super.onInput(self, keys) end +------------------------ +-- QuickMenu +-- + +QuickMenu = defclass(QuickMenu, widgets.Panel) +QuickMenu.ATTRS{ + frame_style=gui.FRAME_INTERIOR, + frame_background=gui.CLEAR_PEN, + visible=false, + multiselect=false, + label=DEFAULT_NIL, + choices_fn=DEFAULT_NIL, +} + +function QuickMenu:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text=self.label, + }, + widgets.FilteredList{ + view_id='list', + frame={t=2, l=0, b=self.multiselect and 3 or 0}, + on_submit=function(_, choice) + choice.fn() + self:hide() + end, + on_submit2=self.multiselect and function(_, choice) + choice.fn() + local list = self.subviews.list + local filter = list:getFilter() + list:setChoices(self.choices_fn(), list:getSelected()) + list:setFilter(filter) + end or nil, + }, + widgets.Label{ + frame={b=1, l=0}, + text='Shift click to select multiple.', + visible=self.multiselect, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + key='CUSTOM_CTRL_A', + label='Select all', + visible=self.multiselect, + on_activate=function() + local list = self.subviews.list + for _,choice in ipairs(list:getVisibleChoices()) do + choice.fn() + end + local filter = list:getFilter() + list:setChoices(self.choices_fn(), list:getSelected()) + list:setFilter(filter) + end, + }, + } +end + +function QuickMenu:show() + self.prev_focus_owner = self.focus_group.cur + self.visible = true + local list = self.subviews.list + list.edit:setText('') + list.edit:setFocus(true) + list:setChoices(self.choices_fn()) +end + +function QuickMenu:hide() + self.visible = false + if self.prev_focus_owner then + self.prev_focus_owner:setFocus(true) + end +end + +function QuickMenu:onInput(keys) + if ColumnMenu.super.onInput(self, keys) then + return true + end + if keys._MOUSE_R then + self:hide() + elseif keys._MOUSE_L and not self:getMouseFramePos() then + self:hide() + end + return true +end + ------------------------ -- Manipulator -- @@ -968,17 +1063,31 @@ function Manipulator:init() widgets.HotkeyLabel{ frame={b=2, l=22}, auto_width=true, + label='Jump to column', + key='CUSTOM_CTRL_G', + on_activate=function() self.subviews.quick_jump_menu:show() end, + }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + auto_width=true, label='Hide column', key='CUSTOM_SHIFT_H', on_activate=function() self.subviews.sheet:hide_current_col() end, }, widgets.HotkeyLabel{ - frame={b=2, l=38}, + frame={b=1, l=22}, auto_width=true, label='Hide group', key='CUSTOM_CTRL_H', on_activate=function() self.subviews.sheet:hide_current_col_group() end, }, + widgets.HotkeyLabel{ + frame={b=1, l=46}, + auto_width=true, + label='Unhide column', + key='CUSTOM_SHIFT_U', + on_activate=function() self.subviews.unhide_menu:show() end, + }, widgets.HotkeyLabel{ frame={b=0, l=0}, auto_width=true, @@ -995,6 +1104,41 @@ function Manipulator:init() }, }, }, + QuickMenu{ + view_id='quick_jump_menu', + frame={b=0, w=35, h=25}, + label='Jump to column:', + choices_fn=function() + local choices = {} + for idx,col in ipairs(self.subviews.sheet.cols.subviews) do + if col.hidden then goto continue end + table.insert(choices, { + text=('%s/%s'):format(col.group, col.label), + fn=function() self.subviews.sheet:jump_to_col(idx) end, + }) + ::continue:: + end + return choices + end, + }, + QuickMenu{ + view_id='unhide_menu', + frame={b=0, w=35, h=25}, + multiselect=true, + label='Unhide column:', + choices_fn=function() + local choices = {} + for idx,col in ipairs(self.subviews.sheet.cols.subviews) do + if not col.hidden then goto continue end + table.insert(choices, { + text=('%s/%s'):format(col.group, col.label), + fn=function() self.subviews.sheet.cols.subviews[idx]:unhide_column() end, + }) + ::continue:: + end + return choices + end, + }, } end From 03030cdf0c60b92dbcc48efc52daa382823c9c13 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 02:35:38 -0700 Subject: [PATCH 175/811] implement zooming to unit and buildings --- gui/manipulator.lua | 50 +++++++++++++++++++++++++++++++++++++++------ 1 file changed, 44 insertions(+), 6 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 9b233dae99..2932ee2e75 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -95,6 +95,12 @@ function ColumnMenu:init() text='Hide group', fn=self.col:callback('hide_group'), }) + if self.col.zoom_fn then + table.insert(choices, { + text='Zoom to', + fn=self.col.zoom_fn, + }) + end self:addviews{ widgets.List{ @@ -148,6 +154,7 @@ Column.ATTRS{ count_fn=DEFAULT_NIL, cmp_fn=DEFAULT_NIL, choice_fn=DEFAULT_NIL, + zoom_fn=DEFAULT_NIL, } local CH_DOT = string.char(15) @@ -233,7 +240,7 @@ function Column:init() }, ColumnMenu{ view_id='col_menu', - frame={l=0, t=1, h=5}, + frame={l=0, t=1, h=self.zoom_fn and 6 or 5}, col=self, }, }, @@ -647,19 +654,23 @@ function Spreadsheet:init() end local function add_workshops(vec, type_enum, type_defs) - for _, workshop in ipairs(vec) do + for _, bld in ipairs(vec) do cols:addviews{ ToggleColumn{ group='workshops', - label=get_workshop_label(workshop, type_enum, type_defs), + label=get_workshop_label(bld, type_enum, type_defs), shared=self.shared, - data_fn=curry(toggle_sorted_vec_data, workshop.profile.permitted_workers), + data_fn=curry(toggle_sorted_vec_data, bld.profile.permitted_workers), toggle_fn=function(unit_id, prev_val) if not prev_val then -- there can be only one - workshop.profile.permitted_workers:resize(0) + bld.profile.permitted_workers:resize(0) end - toggle_sorted_vec(workshop.profile.permitted_workers, unit_id, prev_val) + toggle_sorted_vec(bld.profile.permitted_workers, unit_id, prev_val) + end, + zoom_fn=function() + dfhack.gui.revealInDwarfmodeMap( + xyz2pos(bld.centerx, bld.centery, bld.z), true, true) end, } } @@ -717,6 +728,19 @@ function Spreadsheet:init() self.namelist:setFocus(true) end +function Spreadsheet:zoom_to_unit() + local idx = self.namelist:getSelected() + if not idx then return end + local unit = df.unit.find(self.subviews.name:get_sorted_unit_id(idx)) + if not unit then return end + dfhack.gui.revealInDwarfmodeMap( + xyz2pos(dfhack.units.getPosition(unit)), true, true) +end + +function Spreadsheet:zoom_to_col_source() + -- TODO +end + function Spreadsheet:sort_by_current_col() -- TODO end @@ -1091,6 +1115,20 @@ function Manipulator:init() widgets.HotkeyLabel{ frame={b=0, l=0}, auto_width=true, + label='Zoom to unit', + key='CUSTOM_SHIFT_Z', + on_activate=function() self.subviews.sheet:zoom_to_unit() end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=22}, + auto_width=true, + label='Zoom to col source', + key='CUSTOM_CTRL_Z', + on_activate=function() self.subviews.sheet:zoom_to_col_source() end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=46}, + auto_width=true, label=function() return self.needs_refresh and 'Refresh units (new units have arrived)' or 'Refresh units' end, From 3ecf6e039b08578e9cc77f26118acb544823cc70 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 02:54:09 -0700 Subject: [PATCH 176/811] disable non-functional hotkey buttons --- gui/manipulator.lua | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 2932ee2e75..0fffdb1079 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -34,8 +34,7 @@ end -- preset schema local function get_default_preset() return { - hidden_groups={}, - hidden_cols={}, + cols={}, pinned={}, } end @@ -737,6 +736,7 @@ function Spreadsheet:zoom_to_unit() xyz2pos(dfhack.units.getPosition(unit)), true, true) end +-- TODO these are dependent on having a column cursor function Spreadsheet:zoom_to_col_source() -- TODO end @@ -753,6 +753,10 @@ function Spreadsheet:hide_current_col_group() -- TODO end +function Spreadsheet:export() + -- TODO +end + function Spreadsheet:jump_to_group(group) for i, col in ipairs(self.cols.subviews) do if not col.hidden and col.group == group then @@ -1083,6 +1087,7 @@ function Manipulator:init() label='Sort/reverse sort', key='CUSTOM_SHIFT_S', on_activate=function() self.subviews.sheet:sort_by_current_col() end, + enabled=false, }, widgets.HotkeyLabel{ frame={b=2, l=22}, @@ -1091,12 +1096,21 @@ function Manipulator:init() key='CUSTOM_CTRL_G', on_activate=function() self.subviews.quick_jump_menu:show() end, }, + widgets.HotkeyLabel{ + frame={b=2, l=46}, + auto_width=true, + label='Export to csv', + key='CUSTOM_SHIFT_E', + on_activate=function() self.subviews.sheet:export() end, + enabled=false, + }, widgets.HotkeyLabel{ frame={b=1, l=0}, auto_width=true, label='Hide column', key='CUSTOM_SHIFT_H', on_activate=function() self.subviews.sheet:hide_current_col() end, + enabled=false, }, widgets.HotkeyLabel{ frame={b=1, l=22}, @@ -1104,6 +1118,7 @@ function Manipulator:init() label='Hide group', key='CUSTOM_CTRL_H', on_activate=function() self.subviews.sheet:hide_current_col_group() end, + enabled=false, }, widgets.HotkeyLabel{ frame={b=1, l=46}, @@ -1122,9 +1137,10 @@ function Manipulator:init() widgets.HotkeyLabel{ frame={b=0, l=22}, auto_width=true, - label='Zoom to col source', + label='Zoom to source', key='CUSTOM_CTRL_Z', on_activate=function() self.subviews.sheet:zoom_to_col_source() end, + enabled=false, }, widgets.HotkeyLabel{ frame={b=0, l=46}, @@ -1133,7 +1149,7 @@ function Manipulator:init() return self.needs_refresh and 'Refresh units (new units have arrived)' or 'Refresh units' end, text_pen=function() - return self.needs_refresh and COLOR_LIGHTRED or COLOR_GRAY + return self.needs_refresh and COLOR_LIGHTRED or COLOR_WHITE end, key='CUSTOM_SHIFT_R', on_activate=function() From 97e7f8c8d86d9f7d07801f090724be1cd2e5bc3f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 09:55:50 -0700 Subject: [PATCH 177/811] shift displayed columns when all columns on screen are hidden --- gui/manipulator.lua | 56 ++++++++++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 18 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 0fffdb1079..5cc4ed666f 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -141,12 +141,15 @@ end -- Column -- +local DEFAULT_DATA_WIDTH = 4 +local DEFAULT_COL_OVERSCAN = 14 + Column = defclass(Column, widgets.Panel) Column.ATTRS{ label=DEFAULT_NIL, group='', label_inset=0, - data_width=4, + data_width=DEFAULT_DATA_WIDTH, hidden=DEFAULT_NIL, shared=DEFAULT_NIL, data_fn=DEFAULT_NIL, @@ -161,7 +164,7 @@ local CH_UP = string.char(30) local CH_DN = string.char(31) function Column:init() - self.frame = utils.assign({t=0, b=0, l=0, w=14}, self.frame or {}) + self.frame = utils.assign({t=0, b=0, l=0, w=DEFAULT_COL_OVERSCAN}, self.frame or {}) local function show_menu() self.subviews.col_menu:show() @@ -767,7 +770,27 @@ function Spreadsheet:jump_to_group(group) end function Spreadsheet:jump_to_col(idx) + idx = math.min(idx, #self.cols.subviews) + idx = math.max(idx, 1) self.left_col = idx + if self.cols.subviews[idx].hidden then + local found = false + for shifted_idx=self.left_col-1,1,-1 do + if not self.cols.subviews[shifted_idx].hidden then + self.left_col = shifted_idx + found = true + break + end + end + if not found then + for shifted_idx=self.left_col+1,#self.cols.subviews do + if not self.cols.subviews[shifted_idx].hidden then + self.left_col = shifted_idx + break + end + end + end + end self:updateLayout() end @@ -868,7 +891,11 @@ function Spreadsheet:render(dc) if self.shared.refresh_headers then self:update_headers() end - self:updateLayout() + if self.cols.subviews[self.left_col].hidden then + self:jump_to_col(self.left_col) + else + self:updateLayout() + end end local page_top = self.namelist.page_top local selected = self.namelist:getSelected() @@ -881,23 +908,16 @@ function Spreadsheet:render(dc) end function Spreadsheet:get_num_visible_cols() - local count = 0 - for _,col in ipairs(self.cols.subviews) do - if col.visible then - count = count + 1 - end - end - return count + local rect = self.frame_rect + if not rect then return 1 end + local other_width = self.subviews.name.data_width + (DEFAULT_COL_OVERSCAN - DEFAULT_DATA_WIDTH) + local width = rect.width - other_width + return width // (DEFAULT_DATA_WIDTH + 1) end function Spreadsheet:onInput(keys) if keys.KEYBOARD_CURSOR_LEFT then - for idx=self.left_col-1,1,-1 do - if not self.cols.subviews[idx].hidden then - self:jump_to_col(idx) - break - end - end + self:jump_to_col(self.left_col-1) elseif keys.KEYBOARD_CURSOR_LEFT_FAST then local remaining = self:get_num_visible_cols() local target_col = self.left_col @@ -1253,14 +1273,14 @@ ManipulatorOverlay.ATTRS{ default_pos={x=50, y=-6}, default_enabled=true, viewscreens='dwarfmode/Info/CREATURES/CITIZEN', - frame={w=34, h=1}, + frame={w=35, h=1}, } function ManipulatorOverlay:init() self:addviews{ widgets.TextButton{ frame={t=0, l=0}, - label='DFHack citizen interface', + label='DFHack citizen management', key='CUSTOM_CTRL_N', on_activate=function() dfhack.run_script('gui/manipulator') end, }, From ce186331c5200b93426202215ee823ec0255bcec Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 May 2024 11:03:19 -0700 Subject: [PATCH 178/811] keyboard cursor prototype and export logic --- gui/manipulator.lua | 69 +++++++++++++++++++++++++++++++++++++-------- 1 file changed, 58 insertions(+), 11 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 5cc4ed666f..722b69ccff 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -472,6 +472,7 @@ local function toggle_choice(get_ordered_data_fn) get_enabled_button_token(ENABLED_PEN_LEFT, DISABLED_PEN_LEFT), get_enabled_button_token(ENABLED_PEN_CENTER, DISABLED_PEN_CENTER), get_enabled_button_token(ENABLED_PEN_RIGHT, DISABLED_PEN_RIGHT), + ' ', }, } end @@ -562,6 +563,7 @@ function Spreadsheet:init() sort_stack={}, sort_order={}, -- list of indices into filtered_unit_ids (or cache.filtered_units) cache={}, -- cached pointers; reset at end of frame + cur_col=nil, refresh_units=true, refresh_headers=true, } @@ -718,6 +720,9 @@ function Spreadsheet:init() self.shared.sort_stack[1] = {col=self.subviews.name, rev=false} self.shared.sort_stack[2] = {col=self.subviews.favorites, rev=false} + -- set initial selection + self:set_cur_col(self.subviews.favorites) + self.namelist = self.subviews.name.subviews.col_list self:addviews{ widgets.Scrollbar{ @@ -730,6 +735,16 @@ function Spreadsheet:init() self.namelist:setFocus(true) end +local CURSOR_PEN = dfhack.pen.parse{fg=COLOR_GREY, bg=COLOR_CYAN} + +function Spreadsheet:set_cur_col(col) + if self.shared.cur_col then + self.shared.cur_col.subviews.col_list.cursor_pen = COLOR_LIGHTCYAN + end + self.shared.cur_col = col + col.subviews.col_list.cursor_pen = CURSOR_PEN +end + function Spreadsheet:zoom_to_unit() local idx = self.namelist:getSelected() if not idx then return end @@ -739,25 +754,62 @@ function Spreadsheet:zoom_to_unit() xyz2pos(dfhack.units.getPosition(unit)), true, true) end --- TODO these are dependent on having a column cursor function Spreadsheet:zoom_to_col_source() - -- TODO + if not self.shared.cur_col or not self.shared.cur_col.zoom_fn then return end + self.shared.cur_col.zoom_fn() end function Spreadsheet:sort_by_current_col() - -- TODO + if not self.shared.cur_col then return end + self.shared.cur_col:sort(true) end function Spreadsheet:hide_current_col() - -- TODO + if not self.shared.cur_col then return end + self.shared.cur_col:hide_column() end function Spreadsheet:hide_current_col_group() - -- TODO + if not self.shared.cur_col then return end + self.shared.cur_col:hide_group() end +-- utf8-ize and, if needed, quote and escape +local function make_csv_cell(fmt, ...) + local str = fmt:format(...) + str = dfhack.df2utf(str) + if str:find('[,"]') then + str = str:gsub('"', '""') + str = ('"%s"'):format(str) + end + return str +end + +-- exports visible data, in the current sort, to a .csv file function Spreadsheet:export() - -- TODO + local file = io.open('manipulator.csv', 'a+') + if not file then + dfhack.printerr('could not open export file: manipulator.csv') + return + end + file:write(make_csv_cell('%s,', self.subviews.name.label)) + for _, col in ipairs(self.cols.subviews) do + if col.hidden then goto continue end + file:write(make_csv_cell('%s/%s,', col.group, col.label)) + ::continue:: + end + file:write(NEWLINE) + for row=1,#self.shared.filtered_unit_ids do + file:write(make_csv_cell('%s', self.subviews.name:get_sorted_data(row))) + file:write(',') + for _, col in ipairs(self.cols.subviews) do + if col.hidden then goto continue end + file:write(make_csv_cell('%s,', col:get_sorted_data(row) or '')) + ::continue:: + end + file:write(NEWLINE) + end + file:close() end function Spreadsheet:jump_to_group(group) @@ -1107,7 +1159,6 @@ function Manipulator:init() label='Sort/reverse sort', key='CUSTOM_SHIFT_S', on_activate=function() self.subviews.sheet:sort_by_current_col() end, - enabled=false, }, widgets.HotkeyLabel{ frame={b=2, l=22}, @@ -1122,7 +1173,6 @@ function Manipulator:init() label='Export to csv', key='CUSTOM_SHIFT_E', on_activate=function() self.subviews.sheet:export() end, - enabled=false, }, widgets.HotkeyLabel{ frame={b=1, l=0}, @@ -1130,7 +1180,6 @@ function Manipulator:init() label='Hide column', key='CUSTOM_SHIFT_H', on_activate=function() self.subviews.sheet:hide_current_col() end, - enabled=false, }, widgets.HotkeyLabel{ frame={b=1, l=22}, @@ -1138,7 +1187,6 @@ function Manipulator:init() label='Hide group', key='CUSTOM_CTRL_H', on_activate=function() self.subviews.sheet:hide_current_col_group() end, - enabled=false, }, widgets.HotkeyLabel{ frame={b=1, l=46}, @@ -1160,7 +1208,6 @@ function Manipulator:init() label='Zoom to source', key='CUSTOM_CTRL_Z', on_activate=function() self.subviews.sheet:zoom_to_col_source() end, - enabled=false, }, widgets.HotkeyLabel{ frame={b=0, l=46}, From c47f79de920ce70cee03bfdc2cd51167da662bbd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 22 May 2024 07:18:24 -0700 Subject: [PATCH 179/811] progress towards keyboard cursor --- gui/manipulator.lua | 37 ++++++++++++++++++++++--------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 722b69ccff..2e2c901d03 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -262,6 +262,7 @@ function Column:on_select(idx, choice) -- avoiding an infinite loop local namelist = self.parent_view.parent_view.namelist if namelist then + self.shared.set_cursor_col_fn(self) namelist:setSelected(idx) end end @@ -563,7 +564,8 @@ function Spreadsheet:init() sort_stack={}, sort_order={}, -- list of indices into filtered_unit_ids (or cache.filtered_units) cache={}, -- cached pointers; reset at end of frame - cur_col=nil, + cursor_col=nil, + set_cursor_col_fn=self:callback('set_cursor_col'), refresh_units=true, refresh_headers=true, } @@ -716,12 +718,17 @@ function Spreadsheet:init() cols, } + -- teach each column about its relative position so we can track cursor movement + for idx, col in ipairs(cols) do + col.idx = idx + end + -- set up initial sort: primary favorites, secondary name self.shared.sort_stack[1] = {col=self.subviews.name, rev=false} self.shared.sort_stack[2] = {col=self.subviews.favorites, rev=false} - -- set initial selection - self:set_cur_col(self.subviews.favorites) + -- set initial keyboard cursor position + self:set_cursor_col(self.subviews.favorites) self.namelist = self.subviews.name.subviews.col_list self:addviews{ @@ -737,11 +744,11 @@ end local CURSOR_PEN = dfhack.pen.parse{fg=COLOR_GREY, bg=COLOR_CYAN} -function Spreadsheet:set_cur_col(col) - if self.shared.cur_col then - self.shared.cur_col.subviews.col_list.cursor_pen = COLOR_LIGHTCYAN +function Spreadsheet:set_cursor_col(col) + if self.shared.cursor_col then + self.shared.cursor_col.subviews.col_list.cursor_pen = COLOR_LIGHTCYAN end - self.shared.cur_col = col + self.shared.cursor_col = col col.subviews.col_list.cursor_pen = CURSOR_PEN end @@ -755,23 +762,23 @@ function Spreadsheet:zoom_to_unit() end function Spreadsheet:zoom_to_col_source() - if not self.shared.cur_col or not self.shared.cur_col.zoom_fn then return end - self.shared.cur_col.zoom_fn() + if not self.shared.cursor_col or not self.shared.cursor_col.zoom_fn then return end + self.shared.cursor_col.zoom_fn() end function Spreadsheet:sort_by_current_col() - if not self.shared.cur_col then return end - self.shared.cur_col:sort(true) + if not self.shared.cursor_col then return end + self.shared.cursor_col:sort(true) end function Spreadsheet:hide_current_col() - if not self.shared.cur_col then return end - self.shared.cur_col:hide_column() + if not self.shared.cursor_col then return end + self.shared.cursor_col:hide_column() end function Spreadsheet:hide_current_col_group() - if not self.shared.cur_col then return end - self.shared.cur_col:hide_group() + if not self.shared.cursor_col then return end + self.shared.cursor_col:hide_group() end -- utf8-ize and, if needed, quote and escape From 01ee53f5e5c0ff6cc16ade038f0f1408f7158b80 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 5 Oct 2024 19:30:07 -0700 Subject: [PATCH 180/811] mark as unavailable for dark launch --- docs/gui/manipulator.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/gui/manipulator.rst b/docs/gui/manipulator.rst index 0cdbd8990e..353d894c77 100644 --- a/docs/gui/manipulator.rst +++ b/docs/gui/manipulator.rst @@ -3,9 +3,12 @@ gui/manipulator .. dfhack-tool:: :summary: Multi-function unit management interface. - :tags: fort productivity units + :tags: unavailable -This spreadsheet-like UI allows you to see all your units, their skills, properties, assignments, etc. at a glance. You can search, sort, and filter to see exactly the information that you're looking for. Moreover, you can assign units to burrows, squads, work details, and workshops. +This spreadsheet-like UI allows you to see all your units, their skills, +properties, assignments, etc. at a glance. You can search, sort, and filter to +see exactly the information that you're looking for. Moreover, you can assign +units to burrows, squads, work details, and workshops. Usage ----- From b9433de652e485586c2a0effde16be77f12ce554 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 6 Oct 2024 02:35:00 +0000 Subject: [PATCH 181/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- internal/manipulator/presets.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/manipulator/presets.lua b/internal/manipulator/presets.lua index 757e625cda..259d4a43ea 100644 --- a/internal/manipulator/presets.lua +++ b/internal/manipulator/presets.lua @@ -7,4 +7,4 @@ PRESETS = { }, }, -} \ No newline at end of file +} From 48373de4043cc0a93427b1904844711fa6116dc5 Mon Sep 17 00:00:00 2001 From: c3r341 <99835108+c3r341@users.noreply.github.com> Date: Mon, 7 Oct 2024 01:44:22 -0400 Subject: [PATCH 182/811] Update necronomicon.lua --- necronomicon.lua | 36 +++++++++++++++++++++++++++++++++--- 1 file changed, 33 insertions(+), 3 deletions(-) diff --git a/necronomicon.lua b/necronomicon.lua index fcffb0fdb0..91159a48d1 100644 --- a/necronomicon.lua +++ b/necronomicon.lua @@ -79,11 +79,37 @@ function necronomicon(include_slabs) end end +function necronomicon_world(include_slabs) + if include_slabs then + print("Slabs:") + print() + for _,rec in ipairs(df.global.world.artifacts.all) do + if df.item_slabst:is_instance(rec.item) and check_slab_secrets(rec.item) then + print(dfhack.TranslateName(rec.name)) + end + end + print() + end + print("Books and Scrolls:") + print() + for _,rec in ipairs(df.global.world.artifacts.all) do + if df.item_bookst:is_instance(rec.item) or df.item_toolst:is_instance(rec.item) then + local title, interactions = get_book_interactions(rec.item) + + if next(interactions) then + print(" " .. dfhack.df2console(title)) + print_interactions(interactions) + print() + end + end + end +end local help = false -local include_slabs = false +local include_slabs, scan_world = false, false local args = argparse.processArgsGetopt({...}, { {"s", "include-slabs", handler=function() include_slabs = true end}, + {"w", "world", handler=function() scan_world = true end}, {"h", "help", handler=function() help = true end} }) @@ -91,8 +117,12 @@ local cmd = args[1] if help or cmd == "help" then print(dfhack.script_help()) -elseif cmd == nil or cmd == "" then - necronomicon(include_slabs) +elseif not cmd then + if scan_world then + necronomicon_world(include_slabs) + else + necronomicon(include_slabs) + end else print(('necronomicon: Invalid argument: "%s"'):format(cmd)) end From 22b1fa59cabdff2cefcb068a20bcfc8bf70d8f1f Mon Sep 17 00:00:00 2001 From: c3r341 <99835108+c3r341@users.noreply.github.com> Date: Mon, 7 Oct 2024 01:47:47 -0400 Subject: [PATCH 183/811] Update necronomicon.rst --- docs/necronomicon.rst | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/necronomicon.rst b/docs/necronomicon.rst index 5028304ee3..199d04d792 100644 --- a/docs/necronomicon.rst +++ b/docs/necronomicon.rst @@ -23,3 +23,7 @@ Options ``-s``, ``--include-slabs`` Also list slabs that contain the secrets of life and death. Note that dwarves cannot read the secrets from a slab in fort mode. + +``-w``, ``-world`` + Lists ALL secret containing books and scrolls across the entire world, + not just your fortress. From 2996fc5d04b02c33bcb245c11f2e4b3eababefb0 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Oct 2024 22:45:27 +0000 Subject: [PATCH 184/811] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) - [github.com/python-jsonschema/check-jsonschema: 0.29.2 → 0.29.3](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.2...0.29.3) - [github.com/pre-commit/pre-commit-hooks: v4.6.0 → v5.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v4.6.0...v5.0.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 208bd78ba6..d93eae187c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: # shared across repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.2 + rev: 0.29.3 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks @@ -34,6 +34,6 @@ repos: - json # specific to scripts: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.6.0 + rev: v5.0.0 hooks: - id: forbid-new-submodules From d5ab9f9b75df5355e9d356abd4cff653faa7b1ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 1 Oct 2024 18:48:45 +0200 Subject: [PATCH 185/811] Migrate scripts widgets constant to use getter/setter style --- gui/autodump.lua | 2 +- gui/teleport.lua | 2 +- internal/control-panel/registry.lua | 12 ++++++------ internal/journal/text_editor.lua | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) diff --git a/gui/autodump.lua b/gui/autodump.lua index d3ec16c840..7bfa6c8702 100644 --- a/gui/autodump.lua +++ b/gui/autodump.lua @@ -253,7 +253,7 @@ function Autodump:onInput(keys) end local now_ms = dfhack.getTickCount() if same_xyz(pos, self.last_map_click_pos) and - now_ms - self.last_map_click_ms <= widgets.DOUBLE_CLICK_MS then + now_ms - self.last_map_click_ms <= widgets.getDoubleClickMs() then self:reset_double_click() self:do_dump(pos) self.mark = nil diff --git a/gui/teleport.lua b/gui/teleport.lua index 068b416475..cf9bf30d50 100644 --- a/gui/teleport.lua +++ b/gui/teleport.lua @@ -296,7 +296,7 @@ function Teleport:onInput(keys) end local now_ms = dfhack.getTickCount() if same_xyz(pos, self.last_map_click_pos) and - now_ms - self.last_map_click_ms <= widgets.DOUBLE_CLICK_MS then + now_ms - self.last_map_click_ms <= widgets.getDoubleClickMs() then self:reset_double_click() self:do_teleport(pos) self.mark = nil diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 4e0bdd2e67..33f8edb547 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -200,8 +200,8 @@ PREFERENCES_BY_IDX = { desc='How long to wait for the second click of a double click, in ms.', default=500, min=50, - get_fn=function() return widgets.DOUBLE_CLICK_MS end, - set_fn=function(val) widgets.DOUBLE_CLICK_MS = val end, + get_fn=widgets.getDoubleClickMs, + set_fn=widgets.setDoubleClickMs, }, { name='SCROLL_DELAY_MS', @@ -209,8 +209,8 @@ PREFERENCES_BY_IDX = { desc='The delay between events when holding the mouse button down on a scrollbar, in ms.', default=20, min=5, - get_fn=function() return widgets.SCROLL_DELAY_MS end, - set_fn=function(val) widgets.SCROLL_DELAY_MS = val end, + get_fn=widgets.getScrollDelayMs, + set_fn=widgets.setScrollDelayMs, }, { name='SCROLL_INITIAL_DELAY_MS', @@ -218,8 +218,8 @@ PREFERENCES_BY_IDX = { desc='The delay before scrolling quickly when holding the mouse button down on a scrollbar, in ms.', default=300, min=5, - get_fn=function() return widgets.SCROLL_INITIAL_DELAY_MS end, - set_fn=function(val) widgets.SCROLL_INITIAL_DELAY_MS = val end, + get_fn=widgets.getScrollInitialDelayMs, + set_fn=widgets.setScrollInitialDelayMs, }, } diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua index 9815a2f12c..b2319efcb6 100644 --- a/internal/journal/text_editor.lua +++ b/internal/journal/text_editor.lua @@ -551,7 +551,7 @@ function TextEditorView:getMultiLeftClick(x, y) if ( self.last_click.x ~= x or self.last_click.y ~= y or - from_last_click_ms > widgets.DOUBLE_CLICK_MS + from_last_click_ms > widgets.getDoubleClickMs() ) then self.clicks_count = 0; end From 2c31993dd1bebdd56ce20ba96c231bf742805426 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 8 Oct 2024 20:29:39 +0200 Subject: [PATCH 186/811] Refactor `gui/journal` text editor to dedicated `TextArea` widget The widget is now stored in `core`, so its not visible in this PR --- gui/journal.lua | 3 +- internal/journal/text_editor.lua | 908 ----------- internal/journal/wrapped_text.lua | 70 - test/gui/journal.lua | 2461 +---------------------------- 4 files changed, 9 insertions(+), 3433 deletions(-) delete mode 100644 internal/journal/text_editor.lua delete mode 100644 internal/journal/wrapped_text.lua diff --git a/gui/journal.lua b/gui/journal.lua index d14468129e..20ce638f2f 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -5,7 +5,6 @@ local gui = require 'gui' local widgets = require 'gui.widgets' local utils = require 'utils' local json = require 'json' -local text_editor = reqscript('internal/journal/text_editor') local shifter = reqscript('internal/journal/shifter') local table_of_contents = reqscript('internal/journal/table_of_contents') @@ -113,7 +112,7 @@ function JournalWindow:init() interior_b=true, frame_style_t=false, }, - text_editor.TextEditor{ + widgets.TextArea{ view_id='journal_editor', frame={t=1, b=3, l=25, r=0}, resize_min={w=30, h=10}, diff --git a/internal/journal/text_editor.lua b/internal/journal/text_editor.lua deleted file mode 100644 index b2319efcb6..0000000000 --- a/internal/journal/text_editor.lua +++ /dev/null @@ -1,908 +0,0 @@ --- Multiline text editor for gui/journal ---@ module = true - -local gui = require 'gui' -local widgets = require 'gui.widgets' -local wrapped_text = reqscript('internal/journal/wrapped_text') - -local CLIPBOARD_MODE = {LOCAL = 1, LINE = 2} -local HISTORY_ENTRY = { - TEXT_BLOCK = 1, - WHITESPACE_BLOCK = 2, - BACKSPACE = 2, - DELETE = 3, - OTHER = 4 -} - -TextEditorHistory = defclass(TextEditorHistory) - -TextEditorHistory.ATTRS{ - history_size = 25, -} - -function TextEditorHistory:init() - self.past = {} - self.future = {} -end - -function TextEditorHistory:store(history_entry_type, text, cursor) - local last_entry = self.past[#self.past] - - if not last_entry or history_entry_type == HISTORY_ENTRY.OTHER or - last_entry.entry_type ~= history_entry_type then - table.insert(self.past, { - entry_type=history_entry_type, - text=text, - cursor=cursor - }) - end - - self.future = {} - - if #self.past > self.history_size then - table.remove(self.past, 1) - end -end - -function TextEditorHistory:undo(curr_text, curr_cursor) - if #self.past == 0 then - return nil - end - - local history_entry = table.remove(self.past, #self.past) - - table.insert(self.future, { - entry_type=OTHER, - text=curr_text, - cursor=curr_cursor - }) - - if #self.future > self.history_size then - table.remove(self.future, 1) - end - - return history_entry -end - -function TextEditorHistory:redo(curr_text, curr_cursor) - if #self.future == 0 then - return true - end - - local history_entry = table.remove(self.future, #self.future) - - table.insert(self.past, { - entry_type=OTHER, - text=curr_text, - cursor=curr_cursor - }) - - if #self.past > self.history_size then - table.remove(self.past, 1) - end - - return history_entry -end - -TextEditor = defclass(TextEditor, widgets.Panel) - -TextEditor.ATTRS{ - init_text = '', - init_cursor = DEFAULT_NIL, - text_pen = COLOR_LIGHTCYAN, - ignore_keys = {'STRING_A096'}, - select_pen = COLOR_CYAN, - on_text_change = DEFAULT_NIL, - on_cursor_change = DEFAULT_NIL, - one_line_mode = false, - debug = false -} - -function TextEditor:init() - self.render_start_line_y = 1 - - self:addviews{ - TextEditorView{ - view_id='text_area', - frame={l=0,r=3,t=0}, - text=self.init_text, - - text_pen=self.text_pen, - ignore_keys=self.ignore_keys, - select_pen=self.select_pen, - debug=self.debug, - one_line_mode=self.one_line_mode, - - on_text_change=function (val) - self:updateLayout() - if self.on_text_change then - self.on_text_change(val) - end - end, - on_cursor_change=self:callback('onCursorChange') - }, - widgets.Scrollbar{ - view_id='scrollbar', - frame={r=0,t=1}, - on_scroll=self:callback('onScrollbar'), - visible=not self.one_line_mode - } - } - self:setFocus(true) -end - -function TextEditor:getText() - return self.subviews.text_area.text -end - -function TextEditor:getCursor() - return self.subviews.text_area.cursor -end - -function TextEditor:onCursorChange(cursor) - local x, y = self.subviews.text_area.wrapped_text:indexToCoords( - self.subviews.text_area.cursor - ) - - if y >= self.render_start_line_y + self.subviews.text_area.frame_body.height then - self:updateScrollbar( - y - self.subviews.text_area.frame_body.height + 1 - ) - elseif (y < self.render_start_line_y) then - self:updateScrollbar(y) - end - - if self.on_cursor_change then - self.on_cursor_change(cursor) - end -end - -function TextEditor:scrollToCursor(cursor_offset) - if self.subviews.scrollbar.visible then - local _, cursor_liny_y = self.subviews.text_area.wrapped_text:indexToCoords( - cursor_offset - ) - self:updateScrollbar(cursor_liny_y) - end -end - -function TextEditor:setCursor(cursor_offset) - return self.subviews.text_area:setCursor(cursor_offset) -end - -function TextEditor:getPreferredFocusState() - return self.parent_view.focus -end - -function TextEditor:postUpdateLayout() - self:updateScrollbar(self.render_start_line_y) - - if self.subviews.text_area.cursor == nil then - local cursor = self.init_cursor or #self.init_text + 1 - self.subviews.text_area:setCursor(cursor) - self:scrollToCursor(cursor) - end -end - -function TextEditor:onScrollbar(scroll_spec) - local height = self.subviews.text_area.frame_body.height - - local render_start_line = self.render_start_line_y - if scroll_spec == 'down_large' then - render_start_line = render_start_line + math.ceil(height / 2) - elseif scroll_spec == 'up_large' then - render_start_line = render_start_line - math.ceil(height / 2) - elseif scroll_spec == 'down_small' then - render_start_line = render_start_line + 1 - elseif scroll_spec == 'up_small' then - render_start_line = render_start_line - 1 - else - render_start_line = tonumber(scroll_spec) - end - - self:updateScrollbar(render_start_line) -end - -function TextEditor:updateScrollbar(scrollbar_current_y) - local lines_count = #self.subviews.text_area.wrapped_text.lines - - local render_start_line_y = (math.min( - #self.subviews.text_area.wrapped_text.lines - self.subviews.text_area.frame_body.height + 1, - math.max(1, scrollbar_current_y) - )) - - self.subviews.scrollbar:update( - render_start_line_y, - self.frame_body.height, - lines_count - ) - - if (self.frame_body.height >= lines_count) then - render_start_line_y = 1 - end - - self.render_start_line_y = render_start_line_y - self.subviews.text_area:setRenderStartLineY(self.render_start_line_y) -end - -function TextEditor:renderSubviews(dc) - self.subviews.text_area.frame_body.y1 = self.frame_body.y1-(self.render_start_line_y - 1) - - TextEditor.super.renderSubviews(self, dc) -end - -function TextEditor:onInput(keys) - if (self.subviews.scrollbar.is_dragging) then - return self.subviews.scrollbar:onInput(keys) - end - - if keys._MOUSE_L and self:getMousePos() then - self:setFocus(true) - end - - return TextEditor.super.onInput(self, keys) -end - -TextEditorView = defclass(TextEditorView, widgets.Widget) - -TextEditorView.ATTRS{ - text = '', - text_pen = COLOR_LIGHTCYAN, - ignore_keys = {'STRING_A096'}, - pen_selection = COLOR_CYAN, - on_text_change = DEFAULT_NIL, - on_cursor_change = DEFAULT_NIL, - enable_cursor_blink = true, - debug = false, - one_line_mode = false, - history_size = 10, -} - -function TextEditorView:init() - self.sel_end = nil - self.clipboard = nil - self.clipboard_mode = CLIPBOARD_MODE.LOCAL - self.render_start_line_y = 1 - - self.cursor = nil - - self.main_pen = dfhack.pen.parse({ - fg=self.text_pen, - bg=COLOR_RESET, - bold=true - }) - self.sel_pen = dfhack.pen.parse({ - fg=self.text_pen, - bg=self.pen_selection, - bold=true - }) - - self.text = self:normalizeText(self.text) - - self.wrapped_text = wrapped_text.WrappedText{ - text=self.text, - wrap_width=256 - } - - self.history = TextEditorHistory{history_size=self.history_size} -end - -function TextEditorView:normalizeText(text) - if self.one_line_mode then - return text:gsub("\r?\n", "") - end - - return text -end - -function TextEditorView:setRenderStartLineY(render_start_line_y) - self.render_start_line_y = render_start_line_y -end - -function TextEditorView:getPreferredFocusState() - return true -end - -function TextEditorView:postComputeFrame() - self:recomputeLines() -end - -function TextEditorView:recomputeLines() - self.wrapped_text:update( - self.text, - -- something cursor '_' need to be add at the end of a line - self.frame_body.width - 1 - ) -end - -function TextEditorView:setCursor(cursor_offset) - self.cursor = math.max( - 1, - math.min(#self.text + 1, cursor_offset) - ) - - if self.debug then - print('cursor', self.cursor) - end - - self.sel_end = nil - self.last_cursor_x = nil - - if self.on_cursor_change then - self.on_cursor_change(self.cursor) - end -end - -function TextEditorView:setSelection(from_offset, to_offset) - -- text selection is always start on self.cursor and on self.sel_end - self:setCursor(from_offset) - self.sel_end = to_offset - - if self.debug and to_offset then - print('sel_end', to_offset) - end -end - -function TextEditorView:hasSelection() - return not not self.sel_end -end - -function TextEditorView:eraseSelection() - if (self:hasSelection()) then - local from, to = self.cursor, self.sel_end - if (from > to) then - from, to = to, from - end - - local new_text = self.text:sub(1, from - 1) .. self.text:sub(to + 1) - self:setText(new_text) - - self:setCursor(from) - self.sel_end = nil - end -end - -function TextEditorView:setClipboard(text) - dfhack.internal.setClipboardTextCp437Multiline(text) -end - -function TextEditorView:copy() - if self.sel_end then - self.clipboard_mode = CLIPBOARD_MODE.LOCAL - - local from = self.cursor - local to = self.sel_end - - if from > to then - from, to = to, from - end - - self:setClipboard(self.text:sub(from, to)) - - return from, to - else - self.clipboard_mode = CLIPBOARD_MODE.LINE - - local curr_line = self.text:sub( - self:lineStartOffset(), - self:lineEndOffset() - ) - if curr_line:sub(-1,-1) ~= NEWLINE then - curr_line = curr_line .. NEWLINE - end - - self:setClipboard(curr_line) - - return self:lineStartOffset(), self:lineEndOffset() - end -end - -function TextEditorView:cut() - local from, to = self:copy() - if not self:hasSelection() then - self:setSelection(from, to) - end - self:eraseSelection() -end - -function TextEditorView:paste() - local clipboard_lines = dfhack.internal.getClipboardTextCp437Multiline() - local clipboard = table.concat(clipboard_lines, '\n') - if clipboard then - if self.clipboard_mode == CLIPBOARD_MODE.LINE and not self:hasSelection() then - local origin_offset = self.cursor - self:setCursor(self:lineStartOffset()) - self:insert(clipboard) - self:setCursor(#clipboard + origin_offset) - else - self:eraseSelection() - self:insert(clipboard) - end - - end -end - -function TextEditorView:setText(text) - local changed = self.text ~= text - self.text = self:normalizeText(text) - - self:recomputeLines() - - if changed and self.on_text_change then - self.on_text_change(text) - end -end - -function TextEditorView:insert(text) - self:eraseSelection() - local new_text = - self.text:sub(1, self.cursor - 1) .. - text .. - self.text:sub(self.cursor) - - self:setText(new_text) - self:setCursor(self.cursor + #text) -end - -function TextEditorView:onRenderBody(dc) - dc:pen(self.main_pen) - - local max_width = dc.width - local new_line = self.debug and NEWLINE or '' - - local lines_to_render = math.min( - dc.height, - #self.wrapped_text.lines - self.render_start_line_y + 1 - ) - - dc:seek(0, self.render_start_line_y - 1) - for i = self.render_start_line_y, self.render_start_line_y + lines_to_render - 1 do - -- do not render new lines symbol - local line = self.wrapped_text.lines[i]:gsub(NEWLINE, new_line) - dc:string(line) - dc:newline() - end - - local show_focus = not self.enable_cursor_blink - or ( - not self:hasSelection() - and self.parent_view.focus - and gui.blink_visible(530) - ) - - if (show_focus) then - local x, y = self.wrapped_text:indexToCoords(self.cursor) - dc:seek(x - 1, y - 1) - :char('_') - end - - if self:hasSelection() then - local sel_new_line = self.debug and PERIOD or '' - local from, to = self.cursor, self.sel_end - if (from > to) then - from, to = to, from - end - - local from_x, from_y = self.wrapped_text:indexToCoords(from) - local to_x, to_y = self.wrapped_text:indexToCoords(to) - - local line = self.wrapped_text.lines[from_y] - :sub(from_x, to_y == from_y and to_x or nil) - :gsub(NEWLINE, sel_new_line) - - dc:pen(self.sel_pen) - :seek(from_x - 1, from_y - 1) - :string(line) - - for y = from_y + 1, to_y - 1 do - line = self.wrapped_text.lines[y]:gsub(NEWLINE, sel_new_line) - dc:seek(0, y - 1) - :string(line) - end - - if (to_y > from_y) then - local line = self.wrapped_text.lines[to_y] - :sub(1, to_x) - :gsub(NEWLINE, sel_new_line) - dc:seek(0, to_y - 1) - :string(line) - end - - dc:pen({fg=self.text_pen, bg=COLOR_RESET}) - end - - if self.debug then - local cursor_char = self:charAtCursor() - local x, y = self.wrapped_text:indexToCoords(self.cursor) - local debug_msg = string.format( - 'x: %s y: %s ind: %s #line: %s char: %s hist-: %s hist+: %s', - x, - y, - self.cursor, - self:lineEndOffset() - self:lineStartOffset(), - (cursor_char == NEWLINE and 'NEWLINE') or - (cursor_char == ' ' and 'SPACE') or - (cursor_char == '' and 'nil') or - cursor_char, - #self.history.past, - #self.history.future - ) - local sel_debug_msg = self.sel_end and string.format( - 'sel_end: %s', - self.sel_end - ) or '' - - dc:pen({fg=COLOR_LIGHTRED, bg=COLOR_RESET}) - :seek(0, self.parent_view.frame_body.height + self.render_start_line_y - 2) - :string(debug_msg) - :seek(0, self.parent_view.frame_body.height + self.render_start_line_y - 3) - :string(sel_debug_msg) - end -end - -function TextEditorView:charAtCursor() - return self.text:sub(self.cursor, self.cursor) -end - -function TextEditorView:getMultiLeftClick(x, y) - if self.last_click then - local from_last_click_ms = dfhack.getTickCount() - self.last_click.tick - - if ( - self.last_click.x ~= x or - self.last_click.y ~= y or - from_last_click_ms > widgets.getDoubleClickMs() - ) then - self.clicks_count = 0; - end - end - - return self.clicks_count or 0 -end - -function TextEditorView:triggerMultiLeftClick(x, y) - local clicks_count = self:getMultiLeftClick(x, y) - - self.clicks_count = clicks_count + 1 - if (self.clicks_count >= 4) then - self.clicks_count = 1 - end - - self.last_click = { - tick=dfhack.getTickCount(), - x=x, - y=y, - } - return self.clicks_count -end - -function TextEditorView:currentSpacesRange() - -- select "word" only from spaces - local prev_word_end, _ = self.text - :sub(1, self.cursor) - :find('[^%s]%s+$') - local _, next_word_start = self.text:find('%s[^%s]', self.cursor) - - return prev_word_end + 1 or 1, next_word_start - 1 or #self.text -end - -function TextEditorView:currentWordRange() - -- select current word - local _, prev_word_end = self.text - :sub(1, self.cursor - 1) - :find('.*[%s,."\']') - local next_word_start, _ = self.text:find('[%s,."\']', self.cursor) - - return (prev_word_end or 0) + 1, (next_word_start or #self.text + 1) - 1 -end - -function TextEditorView:lineStartOffset(offset) - local loc_offset = offset or self.cursor - return self.text:sub(1, loc_offset - 1):match(".*\n()") or 1 -end - -function TextEditorView:lineEndOffset(offset) - local loc_offset = offset or self.cursor - return self.text:find("\n", loc_offset) or #self.text + 1 -end - -function TextEditorView:wordStartOffset(offset) - return self.text - :sub(1, offset or self.cursor - 1) - :match('.*%s()[^%s]') or 1 -end - -function TextEditorView:wordEndOffset(offset) - return self.text - :match( - '%s*[^%s]*()', - offset or self.cursor - ) or #self.text + 1 -end - -function TextEditorView:onInput(keys) - for _,ignore_key in ipairs(self.ignore_keys) do - if keys[ignore_key] then - return false - end - end - - if self:onMouseInput(keys) then - return true - elseif self:onHistoryInput(keys) then - return true - elseif self:onTextManipulationInput(keys) then - return true - elseif self:onCursorInput(keys) then - return true - elseif keys.CUSTOM_CTRL_C then - self:copy() - return true - elseif keys.CUSTOM_CTRL_X then - self:cut() - self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) - return true - elseif keys.CUSTOM_CTRL_V then - self:paste() - self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) - return true - else - return TextEditor.super.onInput(self, keys) - end -end - -function TextEditorView:onHistoryInput(keys) - if keys.CUSTOM_CTRL_Z then - local history_entry = self.history:undo(self.text, self.cursor) - - if history_entry then - self:setText(history_entry.text) - self:setCursor(history_entry.cursor) - end - - return true - elseif keys.CUSTOM_CTRL_Y then - local history_entry = self.history:redo(self.text, self.cursor) - - if history_entry then - self:setText(history_entry.text) - self:setCursor(history_entry.cursor) - end - - return true - end -end - -function TextEditorView:onMouseInput(keys) - if keys._MOUSE_L then - local mouse_x, mouse_y = self:getMousePos() - if mouse_x and mouse_y then - - local clicks_count = self:triggerMultiLeftClick( - mouse_x + 1, - mouse_y + 1 - ) - if clicks_count == 3 then - self:setSelection( - self:lineStartOffset(), - self:lineEndOffset() - ) - elseif clicks_count == 2 then - local cursor_char = self:charAtCursor() - - local is_white_space = ( - cursor_char == ' ' or cursor_char == NEWLINE - ) - - local from, to - if is_white_space then - from, to = self:currentSpacesRange() - else - from, to = self:currentWordRange() - end - - self:setSelection(from, to) - else - self:setCursor(self.wrapped_text:coordsToIndex( - mouse_x + 1, - mouse_y + 1 - )) - end - - return true - end - - elseif keys._MOUSE_L_DOWN then - - local mouse_x, mouse_y = self:getMousePos() - if mouse_x and mouse_y then - if (self:getMultiLeftClick(mouse_x + 1, mouse_y + 1) > 1) then - return true - end - - local offset = self.wrapped_text:coordsToIndex( - mouse_x + 1, - mouse_y + 1 - ) - - if self.cursor ~= offset then - self:setSelection(self.cursor, offset) - else - self.sel_end = nil - end - - return true - end - end -end - -function TextEditorView:onCursorInput(keys) - if keys.KEYBOARD_CURSOR_LEFT then - self:setCursor(self.cursor - 1) - return true - elseif keys.KEYBOARD_CURSOR_RIGHT then - self:setCursor(self.cursor + 1) - return true - elseif keys.KEYBOARD_CURSOR_UP then - local x, y = self.wrapped_text:indexToCoords(self.cursor) - local last_cursor_x = self.last_cursor_x or x - local offset = y > 1 and - self.wrapped_text:coordsToIndex(last_cursor_x, y - 1) or - 1 - self:setCursor(offset) - self.last_cursor_x = last_cursor_x - return true - elseif keys.KEYBOARD_CURSOR_DOWN then - local x, y = self.wrapped_text:indexToCoords(self.cursor) - local last_cursor_x = self.last_cursor_x or x - local offset = y < #self.wrapped_text.lines and - self.wrapped_text:coordsToIndex(last_cursor_x, y + 1) or - #self.text + 1 - self:setCursor(offset) - self.last_cursor_x = last_cursor_x - return true - elseif keys.CUSTOM_CTRL_HOME then - self:setCursor(1) - return true - elseif keys.CUSTOM_CTRL_END then - -- go to text end - self:setCursor(#self.text + 1) - return true - elseif keys.CUSTOM_CTRL_LEFT then - -- back one word - local word_start = self:wordStartOffset() - self:setCursor(word_start) - return true - elseif keys.CUSTOM_CTRL_RIGHT then - -- forward one word - local word_end = self:wordEndOffset() - self:setCursor(word_end) - return true - elseif keys.CUSTOM_HOME then - -- line start - self:setCursor( - self:lineStartOffset() - ) - return true - elseif keys.CUSTOM_END then - -- line end - self:setCursor( - self:lineEndOffset() - ) - return true - end -end - -function TextEditorView:onTextManipulationInput(keys) - if keys.SELECT then - -- handle enter - if not self.one_line_mode then - self.history:store( - HISTORY_ENTRY.WHITESPACE_BLOCK, - self.text, - self.cursor - ) - self:insert(NEWLINE) - end - - return true - - elseif keys._STRING then - if keys._STRING == 0 then - -- handle backspace - self.history:store(HISTORY_ENTRY.BACKSPACE, self.text, self.cursor) - - if (self:hasSelection()) then - self:eraseSelection() - else - if (self.cursor == 1) then - return true - end - - self:setSelection( - self.cursor - 1, - self.cursor - 1 - ) - self:eraseSelection() - end - - else - local cv = string.char(keys._STRING) - - if (self:hasSelection()) then - self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) - self:eraseSelection() - else - local entry_type = cv == ' ' and HISTORY_ENTRY.WHITESPACE_BLOCK - or HISTORY_ENTRY.TEXT_BLOCK - self.history:store(entry_type, self.text, self.cursor) - end - - self:insert(cv) - end - - return true - elseif keys.CUSTOM_CTRL_A then - -- select all - self:setSelection(1, #self.text) - return true - elseif keys.CUSTOM_CTRL_U then - -- delete current line - self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) - - if (self:hasSelection()) then - -- delete all lines that has selection - self:setSelection( - self:lineStartOffset(self.cursor), - self:lineEndOffset(self.sel_end) - ) - self:eraseSelection() - else - self:setSelection( - self:lineStartOffset(), - self:lineEndOffset() - ) - self:eraseSelection() - end - - return true - elseif keys.CUSTOM_CTRL_K then - -- delete from cursor to end of current line - self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) - - local line_end = self:lineEndOffset(self.sel_end or self.cursor) - 1 - self:setSelection( - self.cursor, - math.max(line_end, self.cursor) - ) - self:eraseSelection() - - return true - elseif keys.CUSTOM_DELETE then - self.history:store(HISTORY_ENTRY.DELETE, self.text, self.cursor) - - if (self:hasSelection()) then - self:eraseSelection() - else - self:setText( - self.text:sub(1, self.cursor - 1) .. - self.text:sub(self.cursor + 1) - ) - end - - return true - elseif keys.CUSTOM_CTRL_W then - -- delete one word backward - self.history:store(HISTORY_ENTRY.OTHER, self.text, self.cursor) - - if not self:hasSelection() and self.cursor ~= 1 then - self:setSelection( - self:wordStartOffset(), - math.max(self.cursor - 1, 1) - ) - end - self:eraseSelection() - - return true - end -end diff --git a/internal/journal/wrapped_text.lua b/internal/journal/wrapped_text.lua deleted file mode 100644 index 68001c877a..0000000000 --- a/internal/journal/wrapped_text.lua +++ /dev/null @@ -1,70 +0,0 @@ ---@ module = true - --- This class caches lines of text wrapped to a specified width for performance --- and readability. It can convert a given text index to (x, y) coordinates in --- the wrapped text and vice versa. - --- Usage: --- This class should only be used in the following scenarios. --- 1. When text or text features need to be rendered --- (wrapped {x, y} coordinates are required). --- 2. When mouse input needs to be converted to the original text position. - --- Using this class in other scenarios may lead to issues with the component's --- behavior when the text is wrapped. -WrappedText = defclass(WrappedText) - -WrappedText.ATTRS{ - text = '', - wrap_width = DEFAULT_NIL, -} - -function WrappedText:init() - self:update(self.text, self.wrap_width) -end - -function WrappedText:update(text, wrap_width) - self.lines = text:wrap( - wrap_width, - { - return_as_table=true, - keep_trailing_spaces=true, - keep_original_newlines=true - } - ) -end - -function WrappedText:coordsToIndex(x, y) - local offset = 0 - - local normalized_y = math.max( - 1, - math.min(y, #self.lines) - ) - - local line_bonus_length = normalized_y == #self.lines and 1 or 0 - local normalized_x = math.max( - 1, - math.min(x, #self.lines[normalized_y] + line_bonus_length) - ) - - for i=1, normalized_y - 1 do - offset = offset + #self.lines[i] - end - - return offset + normalized_x -end - -function WrappedText:indexToCoords(index) - local offset = index - - for y, line in ipairs(self.lines) do - local line_bonus_length = y == #self.lines and 1 or 0 - if offset <= #line + line_bonus_length then - return offset, y - end - offset = offset - #line - end - - return #self.lines[#self.lines] + 1, #self.lines -end diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 612e860785..c05551b0f2 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -95,7 +95,6 @@ local function arrange_empty_journal(options) journal_window.frame.h = options.h + 6 end - local text_area = journal_window.subviews.text_area text_area.enable_cursor_blink = false @@ -163,2046 +162,16 @@ local function read_selected_text(text_area) end end - return text:gsub("\n+$", "") -end - -function test.load() - local journal, text_area = arrange_empty_journal() - text_area:setText(' ') - journal:onRender() - - expect.eq('dfhack/lua/journal', dfhack.gui.getCurFocus(true)[1]) - expect.eq(read_rendered_text(text_area), '_') - - journal:dismiss() -end - -function test.load_input_multiline_text() - local journal, text_area, journal_window = arrange_empty_journal({w=80}) - - local text = table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'Pellentesque dignissim volutpat orci, sed molestie metus elementum vel.', - 'Donec sit amet mattis ligula, ac vestibulum lorem.', - }, '\n') - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), text .. '_') - - journal:dismiss() -end - -function test.handle_numpad_numbers_as_text() - local journal, text_area, journal_window = arrange_empty_journal({w=80}) - - local text = table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - simulate_input_text(text) - - simulate_input_keys({ - STANDARDSCROLL_LEFT = true, - KEYBOARD_CURSOR_LEFT = true, - _STRING = 52, - STRING_A052 = true, - }) - - expect.eq(read_rendered_text(text_area), text .. '4_') - - simulate_input_keys({ - STRING_A054 = true, - STANDARDSCROLL_RIGHT = true, - KEYBOARD_CURSOR_RIGHT = true, - _STRING = 54, - }) - expect.eq(read_rendered_text(text_area), text .. '46_') - - simulate_input_keys({ - KEYBOARD_CURSOR_DOWN = true, - STRING_A050 = true, - _STRING = 50, - STANDARDSCROLL_DOWN = true, - }) - - expect.eq(read_rendered_text(text_area), text .. '462_') - - simulate_input_keys({ - KEYBOARD_CURSOR_UP = true, - STRING_A056 = true, - STANDARDSCROLL_UP = true, - _STRING = 56, - }) - - expect.eq(read_rendered_text(text_area), text .. '4628_') - journal:dismiss() -end - -function test.wrap_text_to_available_width() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor est pellentesque ac.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac._', - }, '\n')); - - journal:dismiss() -end - -function test.submit_new_line() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('SELECT') - simulate_input_keys('SELECT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '', - '_', - }, '\n')); - - text_area:setCursor(58) - journal:onRender() - - simulate_input_keys('SELECT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'el', - '_t.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - -- empty end lines are not rendered - }, '\n')); - - text_area:setCursor(84) - journal:onRender() - - simulate_input_keys('SELECT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'el', - 'it.', - '112: Sed consectetur,', - -- wrapping changed - '_urna sit amet aliquet egestas, ante nibh porttitor ', - 'mi, vitae rutrum eros metus nec libero.', - -- empty end lines are not rendered - }, '\n')); - - journal:dismiss() -end - -function test.keyboard_arrow_up_navigation() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor est pellentesque ac.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim _uismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim li_ero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP') - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor _i, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP') - simulate_input_keys('KEYBOARD_CURSOR_UP') - simulate_input_keys('KEYBOARD_CURSOR_UP') - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur_ urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - journal:dismiss() -end - -function test.keyboard_arrow_down_navigation() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor est pellentesque ac.', - }, '\n') - - simulate_input_text(text) - text_area:setCursor(11) - journal:onRender() - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem _psum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit._', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed c_nsectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellen_esque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac._', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin _ignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - journal:dismiss() -end - -function test.keyboard_arrow_left_navigation() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero_', - }, '\n')); - - for i=1,6 do - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - '_ibero.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec_', - 'libero.', - }, '\n')); - - for i=1,105 do - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit._', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,60 do - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - journal:dismiss() -end - -function test.keyboard_arrow_right_navigation() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '6_: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,53 do - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing_', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - '_lit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,5 do - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit._', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,113 do - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - }, '\n')); - - journal:dismiss() -end - -function test.handle_backspace() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('STRING_A000') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero_', - }, '\n')); - - for i=1,3 do - simulate_input_keys('STRING_A000') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec lib_', - }, '\n')); - - text_area:setCursor(62) - journal:onRender() - - simulate_input_keys('STRING_A000') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit._12: Sed consectetur, urna sit amet aliquet ', - 'egestas, ante nibh porttitor mi, vitae rutrum eros ', - 'metus nec lib', - }, '\n')); - - text_area:setCursor(2) - journal:onRender() - - simulate_input_keys('STRING_A000') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.112: Sed consectetur, urna sit amet aliquet ', - 'egestas, ante nibh porttitor mi, vitae rutrum eros ', - 'metus nec lib', - }, '\n')); - - simulate_input_keys('STRING_A000') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.112: Sed consectetur, urna sit amet aliquet ', - 'egestas, ante nibh porttitor mi, vitae rutrum eros ', - 'metus nec lib', - }, '\n')); - - journal:dismiss() -end - -function test.handle_delete() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(124) - journal:onRender() - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - '_rttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(123) - journal:onRender() - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante ', - 'nibh_rttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(171) - journal:onRender() - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante ', - 'nibhorttitor mi, vitae rutrum eros metus nec libero._0: Lorem ', - 'ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - for i=1,59 do - simulate_input_keys('CUSTOM_DELETE') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante ', - 'nibhorttitor mi, vitae rutrum eros metus nec libero._', - }, '\n')); - - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante ', - 'nibhorttitor mi, vitae rutrum eros metus nec libero._', - }, '\n')); - - journal:dismiss() -end - -function test.line_end() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('CUSTOM_END') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(70) - journal:onRender() - - simulate_input_keys('CUSTOM_END') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(200) - journal:onRender() - - simulate_input_keys('CUSTOM_END') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_input_keys('CUSTOM_END') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - journal:dismiss() -end - -function test.line_beging() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_HOME') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(173) - journal:onRender() - - simulate_input_keys('CUSTOM_HOME') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '_12: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('CUSTOM_HOME') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.line_delete() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - text_area:setCursor(65) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '_' - }, '\n')); - - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_' - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_' - }, '\n')); - - journal:dismiss() -end - -function test.line_delete_to_end() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - text_area:setCursor(70) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_K') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed_', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_K') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - }, '\n')); - - journal:dismiss() -end - -function test.delete_last_word() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing _', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur _', - }, '\n')); - - text_area:setCursor(82) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed _ urna sit amet aliquet egestas, ante nibh porttitor ', - 'mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur ', - }, '\n')); - - text_area:setCursor(37) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, _ctetur adipiscing elit.', - '112: Sed , urna sit amet aliquet egestas, ante nibh porttitor ', - 'mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur ', - }, '\n')); - - for i=1,6 do - simulate_input_keys('CUSTOM_CTRL_W') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '_ctetur adipiscing elit.', - '112: Sed , urna sit amet aliquet egestas, ante nibh porttitor ', - 'mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur ', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_ctetur adipiscing elit.', - '112: Sed , urna sit amet aliquet egestas, ante nibh porttitor ', - 'mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur ', - }, '\n')); - - journal:dismiss() -end - -function test.jump_to_text_end() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_END') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_END') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - journal:dismiss() -end - -function test.jump_to_text_begin() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_HOME') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_HOME') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.select_all() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.text_key_replace_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 9, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem '); - - simulate_input_text('+') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: +_psum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 6, 1, 6, 2) - - simulate_input_text('!') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: +ipsum dolor sit amet, consectetur adipiscing elit.', - '112: S!_r mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 3, 1, 6, 2) - - simulate_input_text('@') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: +ipsum dolor sit amet, consectetur adipiscing elit.', - '112@_m ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - journal:dismiss() -end - -function test.arrows_reset_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_A') - - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_A') - - simulate_input_keys('KEYBOARD_CURSOR_UP') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_A') - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - expect.eq(read_selected_text(text_area), '') - - journal:dismiss() -end - -function test.click_reset_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_mouse_click(text_area, 4, 0) - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_A') - - simulate_mouse_click(text_area, 4, 8) - expect.eq(read_selected_text(text_area), '') - - journal:dismiss() -end - -function test.line_navigation_reset_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_input_keys('CUSTOM_HOME') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_END') - expect.eq(read_selected_text(text_area), '') - - journal:dismiss() -end - -function test.jump_begin_or_end_reset_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_HOME') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_END') - expect.eq(read_selected_text(text_area), '') - - journal:dismiss() -end - -function test.new_line_override_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 29, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum ero', - }, '\n')); - - simulate_input_keys('SELECT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ', - '_ metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.backspace_delete_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 29, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum ero', - }, '\n')); - - simulate_input_keys('STRING_A000') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: _ metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.delete_char_delete_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 29, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum ero', - }, '\n')); - - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: _ metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.delete_line_delete_selection_lines() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 9, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem '); - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_12: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 4, 1, 29, 2) - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_1: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - journal:dismiss() -end - -function test.delete_line_rest_delete_selection_lines() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 9, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem '); - - simulate_input_keys('CUSTOM_CTRL_K') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: _', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 6, 1, 6, 2) - - simulate_input_keys('CUSTOM_CTRL_K') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ', - '112: S_', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 3, 1, 6, 2) - - simulate_input_keys('CUSTOM_CTRL_K') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ', - '112_', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - journal:dismiss() -end - -function test.delete_last_word_delete_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 9, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem '); - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: _psum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 6, 1, 6, 2) - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ipsum dolor sit amet, consectetur adipiscing elit.', - '112: S_r mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 3, 1, 6, 2) - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ipsum dolor sit amet, consectetur adipiscing elit.', - '112_m ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - journal:dismiss() -end - -function test.single_mouse_click_set_cursor() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_click(text_area, 4, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: _orem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 40, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus ne_ libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 49, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 60, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 0, 10) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 21, 10) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor_sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 63, 10) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - journal:dismiss() -end - -function test.double_mouse_click_select_word() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_mouse_click(text_area, 0, 0) - simulate_mouse_click(text_area, 0, 0) - - expect.eq(read_selected_text(text_area), '60:') - - simulate_mouse_click(text_area, 4, 0) - simulate_mouse_click(text_area, 4, 0) - - expect.eq(read_selected_text(text_area), 'Lorem') - - simulate_mouse_click(text_area, 40, 2) - simulate_mouse_click(text_area, 40, 2) - - expect.eq(read_selected_text(text_area), 'nec') - - simulate_mouse_click(text_area, 58, 3) - simulate_mouse_click(text_area, 58, 3) - expect.eq(read_selected_text(text_area), 'elit') - - simulate_mouse_click(text_area, 60, 3) - simulate_mouse_click(text_area, 60, 3) - expect.eq(read_selected_text(text_area), '.') - - journal:dismiss() -end - -function test.double_mouse_click_select_white_spaces() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = 'Lorem ipsum dolor sit amet, consectetur elit.' - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), text .. '_') - - simulate_mouse_click(text_area, 29, 0) - simulate_mouse_click(text_area, 29, 0) - - expect.eq(read_selected_text(text_area), ' ') - - journal:dismiss() -end - -function test.triple_mouse_click_select_line() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_mouse_click(text_area, 0, 0) - simulate_mouse_click(text_area, 0, 0) - simulate_mouse_click(text_area, 0, 0) - - expect.eq( - read_selected_text(text_area), - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - ) - - simulate_mouse_click(text_area, 4, 0) - simulate_mouse_click(text_area, 4, 0) - simulate_mouse_click(text_area, 4, 0) - - expect.eq( - read_selected_text(text_area), - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - ) - - simulate_mouse_click(text_area, 40, 2) - simulate_mouse_click(text_area, 40, 2) - simulate_mouse_click(text_area, 40, 2) - - expect.eq(read_selected_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_mouse_click(text_area, 58, 3) - simulate_mouse_click(text_area, 58, 3) - simulate_mouse_click(text_area, 58, 3) - - expect.eq( - read_selected_text(text_area), - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - ) - - simulate_mouse_click(text_area, 60, 3) - simulate_mouse_click(text_area, 60, 3) - simulate_mouse_click(text_area, 60, 3) - - expect.eq( - read_selected_text(text_area), - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - ) - - journal:dismiss() -end - -function test.mouse_selection_control() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 29, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem ipsum dolor sit amet') - - simulate_mouse_drag(text_area, 0, 0, 29, 0) - - expect.eq(read_selected_text(text_area), '60: Lorem ipsum dolor sit amet') - - simulate_mouse_drag(text_area, 32, 0, 32, 1) - - expect.eq(read_selected_text(text_area), table.concat({ - 'consectetur adipiscing elit.', - '112: Sed consectetur, urna sit am' - }, '\n')); - - simulate_mouse_drag(text_area, 32, 1, 48, 2) - - expect.eq(read_selected_text(text_area), table.concat({ - 'met aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_mouse_drag(text_area, 42, 2, 59, 3) - - expect.eq(read_selected_text(text_area), table.concat({ - 'libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - }, '\n')); - - simulate_mouse_drag(text_area, 42, 2, 65, 3) - - expect.eq(read_selected_text(text_area), table.concat({ - 'libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - }, '\n')); - - simulate_mouse_drag(text_area, 42, 2, 65, 6) - - expect.eq(read_selected_text(text_area), table.concat({ - 'libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - }, '\n')); - - simulate_mouse_drag(text_area, 42, 2, 42, 6) - - expect.eq(read_selected_text(text_area), table.concat({ - 'libero.', - '60: Lorem ipsum dolor sit amet, consectetur' - }, '\n')); - - journal:dismiss() -end - -function test.copy_and_paste_text_line() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_C') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_mouse_click(text_area, 15, 3) - simulate_input_keys('CUSTOM_CTRL_C') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum_dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 5, 0) - simulate_input_keys('CUSTOM_CTRL_C') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '112: _ed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 6, 0) - simulate_input_keys('CUSTOM_CTRL_C') - simulate_mouse_click(text_area, 5, 6) - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: L_rem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.copy_and_paste_selected_text() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 8, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem') - - simulate_input_keys('CUSTOM_CTRL_C') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem_ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 4, 2) - - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLorem_itor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 0, 0) - - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Lorem_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLoremtitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 60, 4) - - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Lorem60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLoremtitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem_', - }, '\n')); - - journal:dismiss() -end - -function test.cut_and_paste_text_line() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_X') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '_', - }, '\n')); - - simulate_mouse_click(text_area, 0, 0) - simulate_input_keys('CUSTOM_CTRL_X') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 60, 2) - simulate_input_keys('CUSTOM_CTRL_X') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '_', - }, '\n')); - - journal:dismiss() -end - -function test.cut_and_paste_selected_text() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 8, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem') - - simulate_input_keys('CUSTOM_CTRL_X') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem_ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_drag(text_area, 4, 0, 8, 0) - simulate_input_keys('CUSTOM_CTRL_X') - - simulate_mouse_click(text_area, 4, 2) - - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLorem_itor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_drag(text_area, 5, 2, 8, 2) - simulate_input_keys('CUSTOM_CTRL_X') - - simulate_mouse_click(text_area, 0, 0) - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'orem_0: ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLtitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_drag(text_area, 5, 2, 8, 2) - simulate_input_keys('CUSTOM_CTRL_X') + return text:gsub("\n+$", "") +end - simulate_mouse_click(text_area, 60, 4) - simulate_input_keys('CUSTOM_CTRL_V') +function test.load() + local journal, text_area = arrange_empty_journal() + text_area:setText(' ') + journal:onRender() - expect.eq(read_rendered_text(text_area), table.concat({ - 'orem60: ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLr mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.tito_', - }, '\n')); + expect.eq('dfhack/lua/journal', dfhack.gui.getCurFocus(true)[1]) + expect.eq(read_rendered_text(text_area), '_') journal:dismiss() end @@ -2266,219 +235,6 @@ function test.restore_text_between_sessions() journal:dismiss() end -function test.scroll_long_text() - local journal, text_area = arrange_empty_journal({w=100, h=10}) - local scrollbar = journal.subviews.scrollbar - - local text = table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'Nulla ut lacus ut tortor semper consectetur.', - 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex._', - }, '\n')) - - simulate_mouse_click(scrollbar, 0, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - }, '\n')) - - simulate_mouse_click(scrollbar, 0, 0) - simulate_mouse_click(scrollbar, 0, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - }, '\n')) - - simulate_mouse_click(scrollbar, 0, scrollbar.frame_body.height - 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex._', - }, '\n')) - - simulate_mouse_click(scrollbar, 0, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - }, '\n')) - - journal:dismiss() -end - -function test.scroll_follows_cursor() - local journal, text_area = arrange_empty_journal({w=100, h=10}) - local scrollbar = journal.subviews.text_area_scrollbar - - local text = table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'Nulla ut lacus ut tortor semper consectetur.', - 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex._', - }, '\n')) - - simulate_mouse_click(text_area, 0, 8) - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_nteger tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - }, '\n')) - - simulate_input_keys('CUSTOM_CTRL_HOME') - - simulate_mouse_click(text_area, 0, 9) - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Nulla ut lacus ut tortor semper consectetur.', - 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - '_onec quis lectus ac erat placerat eleifend.', - }, '\n')) - - simulate_mouse_click(text_area, 44, 10) - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - '_enean non orci id erat malesuada pharetra.', - }, '\n')) - - simulate_mouse_click(text_area, 0, 2) - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Nulla ut lacus ut tortor semper consectetur._', - 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - }, '\n')) - - journal:dismiss() -end - function test.generate_table_of_contents() local journal, text_area = arrange_empty_journal({w=100, h=10}) @@ -2849,207 +605,6 @@ function test.table_of_contents_keyboard_navigation() journal:dismiss() end -function test.fast_rewind_words_right() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60:_Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem_ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,6 do - simulate_input_keys('CUSTOM_CTRL_RIGHT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing_', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit._', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112:_Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,17 do - simulate_input_keys('CUSTOM_CTRL_RIGHT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - }, '\n')); - - journal:dismiss() -end - -function test.fast_rewind_words_left() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - '_ibero.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus _ec ', - 'libero.', - }, '\n')); - - for i=1,8 do - simulate_input_keys('CUSTOM_CTRL_LEFT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - '_nte nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet _gestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,16 do - simulate_input_keys('CUSTOM_CTRL_LEFT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - journal:dismiss() -end - -function test.fast_rewind_reset_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_LEFT') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_A') - - simulate_input_keys('CUSTOM_CTRL_RIGHT') - expect.eq(read_selected_text(text_area), '') - - journal:dismiss() -end - function test.show_tutorials_on_first_use() local journal, text_area, journal_window = arrange_empty_journal({w=65}) simulate_input_keys('CUSTOM_CTRL_O') From c42a23d029f8f43e8b7628efdd8fe73ab9605908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Fri, 11 Oct 2024 21:51:31 +0200 Subject: [PATCH 187/811] Fix journal test --- test/gui/journal.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/gui/journal.lua b/test/gui/journal.lua index c05551b0f2..d7f0ad4daf 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -95,7 +95,7 @@ local function arrange_empty_journal(options) journal_window.frame.h = options.h + 6 end - local text_area = journal_window.subviews.text_area + local text_area = journal_window.subviews.journal_editor.text_area text_area.enable_cursor_blink = false if not options.save_on_change then From 48d19a9a1be283797fecb6809b650d63a02aac5f Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Wed, 16 Oct 2024 19:54:06 +0200 Subject: [PATCH 188/811] implement suggestions from code review --- docs/immortal-cravings.rst | 1 - immortal-cravings.lua | 59 +++++++++++++++++++++----------------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/docs/immortal-cravings.rst b/docs/immortal-cravings.rst index 2fb851b9d6..2ef43c7209 100644 --- a/docs/immortal-cravings.rst +++ b/docs/immortal-cravings.rst @@ -15,4 +15,3 @@ Usage ----- ``enable immortal-cravings`` -``disable immortal-cravings`` diff --git a/immortal-cravings.lua b/immortal-cravings.lua index c945b44f8f..cb42238119 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -24,8 +24,7 @@ local function findClosest(pos, item_vector, is_good) local dclosest = -1 for _,item in ipairs(item_vector) do if not item.flags.in_job and (not is_good or is_good(item)) then - local x, y, z = dfhack.items.getPosition(item) - local pitem = xyz2pos(x, y, z) + local pitem = xyz2pos(dfhack.items.getPosition(item)) local ditem = distance(pos, pitem) if dfhack.maps.canWalkBetween(pos, pitem) and (not closest or ditem < dclosest) then closest = item @@ -38,11 +37,11 @@ end ---find a drink ---@param pos df.coord ----@return df.item_drinkst|nil +---@return df.item_drinkst? local function get_closest_drink(pos) local is_good = function (drink) local container = dfhack.items.getContainer(drink) - return container and df.item_barrelst:is_instance(container) + return container and container:isFoodStorage() end return findClosest(pos, df.global.world.items.other.DRINK, is_good) end @@ -52,7 +51,12 @@ end local function get_closest_meal(pos) ---@param meal df.item_foodst local function is_good(meal) - return meal.flags.rotten == false + if meal.flags.rotten then + return false + else + local container = dfhack.items.getContainer(meal) + return not container or container:isFoodStorage() + end end return findClosest(pos, df.global.world.items.other.FOOD, is_good) end @@ -123,13 +127,13 @@ local function load_state() enabled = persisted_data.enabled or false end -DrinkAlcohol = df.need_type['DrinkAlcohol'] -EatGoodMeal = df.need_type['EatGoodMeal'] +DrinkAlcohol = df.need_type.DrinkAlcohol +EatGoodMeal = df.need_type.EatGoodMeal ---@type integer[] -watched = {} +watched = watched or {} -threshold = -9000 +local threshold = -9000 ---unit loop: check for idle watched units and create eat/drink jobs for them local function unit_loop() @@ -138,23 +142,25 @@ local function unit_loop() local kept = {} for _, unit_id in ipairs(watched) do local unit = df.unit.find(unit_id) - if unit and not (unit.flags1.caged or unit.flags1.chained) then - if not idle.unitIsAvailable(unit) then - table.insert(kept, unit.id) - else - -- - for _, need in ipairs(unit.status.current_soul.personality.needs) do - if need.id == DrinkAlcohol and need.focus_level < threshold then - goDrink(unit) - goto next_unit - elseif need.id == EatGoodMeal and need.focus_level < threshold then - goEat(unit) - goto next_unit - end + if + not unit or not dfhack.units.isActive(unit) or + unit.flags1.caged or unit.flags1.chained + then + goto next_unit + end + if not idle.unitIsAvailable(unit) then + table.insert(kept, unit.id) + else + -- unit is available for jobs; satisfy one of its needs + for _, need in ipairs(unit.status.current_soul.personality.needs) do + if need.id == DrinkAlcohol and need.focus_level < threshold then + goDrink(unit) + break + elseif need.id == EatGoodMeal and need.focus_level < threshold then + goEat(unit) + break end end - else - -- print('immortal-cravings: unit gone or caged') end ::next_unit:: end @@ -167,7 +173,7 @@ end ---main loop: look for citizens with personality needs for food/drink but w/o physiological need local function main_loop() - print('immortal-cravings watching:') + -- print('immortal-cravings watching:') watched = {} for _, unit in ipairs(dfhack.units.getCitizens()) do if unit.curse.add_tags1.NO_DRINK or unit.curse.add_tags1.NO_EAT then @@ -176,7 +182,7 @@ local function main_loop() need.id == EatGoodMeal and need.focus_level < threshold then table.insert(watched, unit.id) - print(' '..dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit)))) + -- print(' '..dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit)))) goto next_unit end end @@ -208,6 +214,7 @@ end dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_MAP_UNLOADED then enabled = false + -- repeat-util will cancel the loops on unload return end From b18aa8fd4aa313fc3935f6ed62ee4829be4bbcaa Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 21 Oct 2024 09:04:39 -0500 Subject: [PATCH 189/811] Add export map tool --- docs/export-map.rst | 169 +++++++++++++++++++++++++++ export-map.lua | 279 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 448 insertions(+) create mode 100644 docs/export-map.rst create mode 100644 export-map.lua diff --git a/docs/export-map.rst b/docs/export-map.rst new file mode 100644 index 0000000000..fabf543eb3 --- /dev/null +++ b/docs/export-map.rst @@ -0,0 +1,169 @@ +export-map +========== + +.. dfhack-tool:: + :summary: Export fortress map tile data to a JSON file + :tags: dev map + +WARNING - This command will cause the game to freeze for minutes depending on +map size and options enabled. + +Exports the fortress map tile data to a JSON file. (does not include items, +characters, buildings, etc.) Depending on options enabled, there will be a +``KEY`` table in the JSON with relevant [number ID] values that match a number +to their object type. + +Usage +----- + +:: + + export-map [include|exclude] [] + +Examples +-------- + +``export-map`` + Exports the fortress map to JSON with ALL data included + +``export-map include -m -s -v`` + Exports the fortress map to JSON with only materials, shape, and vein data + included + +``export-map exclude --variant --hidden --light`` + Exports the fortress map to JSON with variant, hidden, and light data + excluded + +Required +-------- + +When you are using options, you must include one of these settings. + +``include`` + Include only the data listed from options to the JSON (whitelist) + +``exclude`` + Exclude only the data listed from options to the JSON (blacklist) + +Options +------- + +``help``, ``--help`` + Shows the help menu + +``-t``, ``--tiletype`` + The tile material classification [number ID] (AIR/SOIL/STONE/RIVER/etc.) + +``-s``, ``--shape`` + The tile shape classification [number ID] (EMPTY/FLOOR/WALL/STAIR/etc.) + +``-p``, ``--special`` + The tile surface special properties for smoothness [number ID] + (NORMAL/SMOOTH/ROUGH/etc.) (used for engraving) + +``-r``, ``--variant`` + The specific variant of a tile that have visual variations [number] (like + grass tiles in ASCII mode) + +``-h``, ``--hidden`` + Whether tile is revealed or unrevealed [boolean] + +``-l``, ``--light`` + Whether tile is exposed to light [boolean] + +``-b``, ``--subterranean`` + Whether the tile is considered underground [boolean] (used to determine + crops that can be planted underground) + +``-o``, ``--outside`` + Whether the tile is considered “outside”. [boolean] (used by weather effects + to trigger on outside tiles) + +``-a``, ``--aquifer`` + Whether the tile is considered an aquifer [number ID] (NONE/LIGHT/HEAVY) + +``-m``, ``--material`` + The material inside the tile [number ID] (IRON/GRANITE/CLAY/ + TOPAZOLITE/BLACK_OPAL/etc.) (will return nil if the tile is empty) + +``-u``, ``--underworld`` + Whether the underworld z-levels will be included [boolean] + +JSON DATA +--------- + +``ARGUMENT_OPTION_ORDER`` + The order of the selected options for how data is arranged at a map position + Example 1: + ``{"material": 1, "shape": 2, "hidden": 3}`` + ``map[z][y][x] = {material_data, shape_data, hidden_data}`` + Example 2: + ``{"variant": 3, "light": 1, "outside": 2, "aquifer": 4}`` + ``map[z][y][x] = {light_data, outside_data, variant_data, aquifer_data}`` + +``MAP_SIZE`` + A table containing basic information about the map size for width, height, + depth. (x, y, z) The underworld_z_level is included if the underworld option + is enabled and the map depth (z) will be automatically adjusted. + +``KEYS`` + The tables containing the [number ID] values for different options. + + ``"SHAPE": { + "-1": "NONE", + "0": "EMPTY", + "1": "FLOOR", + "2": "BOULDERS", + "3": "PEBBLES", + "4": "WALL", + ... + "18": "ENDLESS_PIT" + `` + + ``"PLANT": { + "0": "SINGLE-GRAIN_WHEAT", + "1": "TWO-GRAIN_WHEAT", + "2": "SOFT_WHEAT", + "3": "HARD_WHEAT", + "4": "SPELT", + "5": "BARLEY", + ... + "224": "PALM" + `` + + ``"AQUIFER": { + "0": "NONE", + "1": "LIGHT", + "2": "HEAVY" + `` + + Note - when using the ``materials`` option, you need to pair the [number ID] + with the correct ``KEYS`` material table. Generally you use ``tiletype`` + option as a helper to sort tiles into different material types. I would + recommend consulting ``tile-material.lua`` to see how materials are sorted. + +``map`` + JSON map data is arranged as: ``map[z][y][x] = {tile_data}`` + + JSON maps start at index [1]. (starts at map[1][1][1]) + DF maps start at index [0]. (starts at map[0][0][0]) + + To translate an actual DF map position from the JSON map you need add +1 to + all x/y/z coordinates to get the correct tile position. + + The ``ARGUMENT_OPTION_ORDER`` determines order of tile data. (see above) + I would recommend referencing the tile data like so: + ``shape = json_data.map[z][x][y][json_data.ARGUMENT_OPTIONS_ORDER.shape]`` + ``light = json_data.map[z][x][y][json_data.ARGUMENT_OPTIONS_ORDER.light]`` + + Note - some of the bottom z-levels for hell do not have the same + width/height as the default map. So if your map is 190x190, the last hell + z-levels are gonna be like 90x90. + + Instead of returning normal tile data like: + ``map[0][90][90] = {tile_data}`` + + It will return nil instead: + ``map[0][91][91] = nil`` + + So you need to account for this! \ No newline at end of file diff --git a/export-map.lua b/export-map.lua new file mode 100644 index 0000000000..0c600d8c82 --- /dev/null +++ b/export-map.lua @@ -0,0 +1,279 @@ +-- Export fortress map tile data to a JSON file +-- based on export-map.lua by mikerenfro: +-- https://github.com/mikerenfro/df-map-export/blob/main/export-map.lua +-- redux version by timothymtorres + +local tm = require('tile-material') +local utils = require('utils') +local json = require('json') +local argparse = require('argparse') + +local include_underworld_z = false +local underworld_z + +-- the layer of the underworld +for _, feature in ipairs(df.global.world.features.map_features) do + if feature:getType() == df.feature_type.underworld_from_layer then + underworld_z = feature.layer + end +end + +local function classify_tile(options, x, y, z) + -- The last z-levels of hell shrink their x/y size unexpectedly! (ಠ_ಠ) + -- if your map is 190x190, the last hell z-levels are gonna be like 90x90 + if dfhack.maps.getTileType(x, y, z) == nil then + return nil -- Designating the non-tiles of hell to be nil + end + + local tileattrs = df.tiletype.attrs[dfhack.maps.getTileType(x, y, z)] + local tileflags, tile_occupancy = dfhack.maps.getTileFlags(x, y, z) + + local tile_data = {} + + for map_option, position in pairs(options) do + if(map_option == "tiletype") then + tile_data[position] = tileattrs.material + elseif(map_option == "shape") then + tile_data[position] = tileattrs.shape + elseif(map_option == "special") then + tile_data[position] = tileattrs.special + elseif(map_option == "variant") then + tile_data[position] = tileattrs.variant + elseif(map_option == "hidden") then + tile_data[position] = tileflags.hidden + elseif(map_option == "light") then + tile_data[position] = tileflags.light + elseif(map_option == "subterranean") then + tile_data[position] = tileflags.subterranean + elseif(map_option == "outside") then + tile_data[position] = tileflags.outside + elseif(map_option == "aquifer") then + -- hardcoding these values bc they are not directly in a list + if(tileflags.water_table and tile_occupancy.heavy_aquifer) then + tile_data[position] = 2 + elseif(tileflags.water_table) then + tile_data[position] = 1 + else + tile_data[position] = 0 + end + elseif(map_option == "material") then + if(tileattrs.material >= 8 and tileattrs.material <= 11) then + -- grass material IDs [8-11] will throw an error so we skip them + tile_data[position] = nil + else + local material = tm.GetTileMat(x, y, z) + tile_data[position] = material and material.index or nil + end + end + end + + return tile_data +end + +local function setup_keys(options) + local KEYS = {} + + if(options.tiletype) then + KEYS.TILETYPE = {} + for id, material in ipairs(df.tiletype_material) do + KEYS.TILETYPE[id] = material + end + end + + if(options.shape) then + KEYS.SHAPE = {} + for id, shape in ipairs(df.tiletype_shape) do + KEYS.SHAPE[id] = shape + end + end + + if(options.special) then + KEYS.SPECIAL = {} + for id, special in ipairs(df.tiletype_special) do + KEYS.SPECIAL[id] = special + end + end + + if(options.variant) then + KEYS.VARIANT = {} + for id, variant in ipairs(df.tiletype_variant) do + KEYS.VARIANT[id] = variant + end + end + + if(options.aquifer) then + -- We are hardcoding since this info is not easily listed anywhere + KEYS.AQUIFER = { + [0] = "NONE", + [1] = "LIGHT", + [2] = "HEAVY", + } + end + + if(options.material) then + KEYS.MATERIAL = {} + KEYS.MATERIAL.PLANT = {} + for id, plant in ipairs(df.global.world.raws.plants.all) do + KEYS.MATERIAL.PLANT[id] = plant.id + end + + KEYS.MATERIAL.SOLID = {} -- everything but plants (stones, gems, metals) + KEYS.MATERIAL.METAL = {} + KEYS.MATERIAL.STONE = {} + KEYS.MATERIAL.GEM = {} + + for id, rock in ipairs(df.global.world.raws.inorganics) do + local material = rock.material + local name = material.state_adj.Solid + KEYS.MATERIAL.SOLID[id] = name +-- cant sort by key see +-- https://stackoverflow.com/questions/26160327/sorting-a-lua-table-by-key + KEYS.MATERIAL.STONE[id] = material.flags.IS_STONE and name or false + KEYS.MATERIAL.GEM[id] = material.flags.IS_GEM and name or false + KEYS.MATERIAL.METAL[id] = material.flags.IS_METAL and name or false + end + end + + return KEYS +end + +local function export_all_z_levels(fortress_name, folder, options) + local xmax, ymax, zmax = dfhack.maps.getTileSize() + local filename = string.format("%s/%s.json", folder, fortress_name) + + if dfhack.filesystem.exists(filename) then + qerror('Destination file ' .. filename .. ' already exists!') + return false + end + + local data = {} + + data.ARGUMENT_OPTION_ORDER = options + data.MAP_SIZE = { + x = xmax, + y = ymax, + -- subtract underworld levels if excluded from options + z = include_underworld_z and zmax or (zmax - underworld_z), + underworld_z_level = include_underworld_z and underworld_z or nil, + } + data.KEYS = setup_keys(options) + + data.map = {} + + local zmin = 0 + if not include_underworld_z then -- skips all z-levels in the underworld + zmin = underworld_z + end + + -- start from bottom z-level (underworld) to top z-level (sky) + for z = zmin, zmax-1 do + local level_data = {} + for y = 0, ymax - 1 do + local row_data = {} + for x = 0, xmax - 1 do + local classification = classify_tile(options, x, y, z) + table.insert(row_data, classification) + end + table.insert(level_data, row_data) + end + table.insert(data.map, level_data) + end + + local f = assert(io.open(filename, 'w')) + f:write(json.encode(data)) + f:close() + print("File created in Dwarf Fortress folder under " .. filename) +end + + +local function export_fortress_map(options) + local fortress_name = dfhack.TranslateName( + df.global.world.world_data.active_site[0].name + ) + local export_path = "map-exports/" .. fortress_name + dfhack.filesystem.mkdir_recursive(export_path) + export_all_z_levels(fortress_name, export_path, options) +end + +if dfhack_flags.module then + return +end + +if not dfhack.isMapLoaded() then + qerror('This script requires a fortress map to be loaded') +end + +local options, args = { + help = false, + tiletype = false, + shape = false, + special = false, + variant = false, + hidden = false, + light = false, + subterranean = false, + outside = false, + aquifer = false, + material = false, +}, {...} + +local positionals = argparse.processArgsGetopt(args, { + {'', 'help', handler=function() options.help = true end}, + {'t', 'tiletype', handler=function() options.tiletype = true end}, + {'s', 'shape', handler=function() options.shape = true end}, + {'p', 'special', handler=function() options.special = true end}, + {'r', 'variant', handler=function() options.variant = true end}, + {'h', 'hidden', handler=function() options.hidden = true end}, + {'l', 'light', handler=function() options.light = true end}, + {'b', 'subterranean', handler=function() options.subterranean = true end}, + {'o', 'outside', handler=function() options.outside = true end}, + {'a', 'aquifer', handler=function() options.aquifer = true end}, + {'m', 'material', handler=function() options.material = true end}, + -- local var since underworld not in ordered option + {'u', 'underworld', handler= function() include_underworld_z = true end}, +}) + +if positionals[1] == "help" or options.help then + print(dfhack.script_help()) + return false +elseif positionals[1] == "include" then + -- no need to change anything +elseif positionals[1] == "exclude" then + for setting in pairs(options) do + options[setting] = not options[setting] + end +else -- include everything + for setting in pairs(options) do + options[setting] = true + end + -- don't forget to include underworld + include_underworld_z = true +end + +local ordered_options = { + "tiletype", + "shape", + "special", + "variant", + "hidden", + "light", + "subterranean", + "outside", + "aquifer", + "material", +} + +-- reorganize ordered options based on selected options via argparse +-- this is so ARGUMENT_OPTION_ORDER has the correct order with no gaps +for setting in pairs(options) do + if not options[setting] then + for pos, json_setting in ipairs(ordered_options) do + if setting == json_setting then + table.remove(ordered_options, pos) + end + end + end +end + +ordered_options = utils.invert(ordered_options) +export_fortress_map(ordered_options) From d60099016ca9ef58860b14f1f5f67bb2f403851f Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 14:41:13 +0000 Subject: [PATCH 190/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/export-map.rst | 52 ++++++++++++++++++++++----------------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/export-map.rst b/docs/export-map.rst index fabf543eb3..85ce6a5863 100644 --- a/docs/export-map.rst +++ b/docs/export-map.rst @@ -5,12 +5,12 @@ export-map :summary: Export fortress map tile data to a JSON file :tags: dev map -WARNING - This command will cause the game to freeze for minutes depending on +WARNING - This command will cause the game to freeze for minutes depending on map size and options enabled. -Exports the fortress map tile data to a JSON file. (does not include items, -characters, buildings, etc.) Depending on options enabled, there will be a -``KEY`` table in the JSON with relevant [number ID] values that match a number +Exports the fortress map tile data to a JSON file. (does not include items, +characters, buildings, etc.) Depending on options enabled, there will be a +``KEY`` table in the JSON with relevant [number ID] values that match a number to their object type. Usage @@ -27,17 +27,17 @@ Examples Exports the fortress map to JSON with ALL data included ``export-map include -m -s -v`` - Exports the fortress map to JSON with only materials, shape, and vein data + Exports the fortress map to JSON with only materials, shape, and vein data included ``export-map exclude --variant --hidden --light`` - Exports the fortress map to JSON with variant, hidden, and light data + Exports the fortress map to JSON with variant, hidden, and light data excluded Required -------- -When you are using options, you must include one of these settings. +When you are using options, you must include one of these settings. ``include`` Include only the data listed from options to the JSON (whitelist) @@ -58,11 +58,11 @@ Options The tile shape classification [number ID] (EMPTY/FLOOR/WALL/STAIR/etc.) ``-p``, ``--special`` - The tile surface special properties for smoothness [number ID] + The tile surface special properties for smoothness [number ID] (NORMAL/SMOOTH/ROUGH/etc.) (used for engraving) ``-r``, ``--variant`` - The specific variant of a tile that have visual variations [number] (like + The specific variant of a tile that have visual variations [number] (like grass tiles in ASCII mode) ``-h``, ``--hidden`` @@ -72,7 +72,7 @@ Options Whether tile is exposed to light [boolean] ``-b``, ``--subterranean`` - Whether the tile is considered underground [boolean] (used to determine + Whether the tile is considered underground [boolean] (used to determine crops that can be planted underground) ``-o``, ``--outside`` @@ -84,7 +84,7 @@ Options ``-m``, ``--material`` The material inside the tile [number ID] (IRON/GRANITE/CLAY/ - TOPAZOLITE/BLACK_OPAL/etc.) (will return nil if the tile is empty) + TOPAZOLITE/BLACK_OPAL/etc.) (will return nil if the tile is empty) ``-u``, ``--underworld`` Whether the underworld z-levels will be included [boolean] @@ -103,15 +103,15 @@ JSON DATA ``MAP_SIZE`` A table containing basic information about the map size for width, height, - depth. (x, y, z) The underworld_z_level is included if the underworld option + depth. (x, y, z) The underworld_z_level is included if the underworld option is enabled and the map depth (z) will be automatically adjusted. ``KEYS`` The tables containing the [number ID] values for different options. ``"SHAPE": { - "-1": "NONE", - "0": "EMPTY", + "-1": "NONE", + "0": "EMPTY", "1": "FLOOR", "2": "BOULDERS", "3": "PEBBLES", @@ -121,7 +121,7 @@ JSON DATA `` ``"PLANT": { - "0": "SINGLE-GRAIN_WHEAT", + "0": "SINGLE-GRAIN_WHEAT", "1": "TWO-GRAIN_WHEAT", "2": "SOFT_WHEAT", "3": "HARD_WHEAT", @@ -132,38 +132,38 @@ JSON DATA `` ``"AQUIFER": { - "0": "NONE", + "0": "NONE", "1": "LIGHT", "2": "HEAVY" `` Note - when using the ``materials`` option, you need to pair the [number ID] - with the correct ``KEYS`` material table. Generally you use ``tiletype`` - option as a helper to sort tiles into different material types. I would + with the correct ``KEYS`` material table. Generally you use ``tiletype`` + option as a helper to sort tiles into different material types. I would recommend consulting ``tile-material.lua`` to see how materials are sorted. ``map`` - JSON map data is arranged as: ``map[z][y][x] = {tile_data}`` - + JSON map data is arranged as: ``map[z][y][x] = {tile_data}`` + JSON maps start at index [1]. (starts at map[1][1][1]) DF maps start at index [0]. (starts at map[0][0][0]) - To translate an actual DF map position from the JSON map you need add +1 to + To translate an actual DF map position from the JSON map you need add +1 to all x/y/z coordinates to get the correct tile position. - The ``ARGUMENT_OPTION_ORDER`` determines order of tile data. (see above) + The ``ARGUMENT_OPTION_ORDER`` determines order of tile data. (see above) I would recommend referencing the tile data like so: ``shape = json_data.map[z][x][y][json_data.ARGUMENT_OPTIONS_ORDER.shape]`` ``light = json_data.map[z][x][y][json_data.ARGUMENT_OPTIONS_ORDER.light]`` - Note - some of the bottom z-levels for hell do not have the same - width/height as the default map. So if your map is 190x190, the last hell + Note - some of the bottom z-levels for hell do not have the same + width/height as the default map. So if your map is 190x190, the last hell z-levels are gonna be like 90x90. Instead of returning normal tile data like: ``map[0][90][90] = {tile_data}`` It will return nil instead: - ``map[0][91][91] = nil`` + ``map[0][91][91] = nil`` - So you need to account for this! \ No newline at end of file + So you need to account for this! From 9cbfcbd95fc4b1bca590d114747cd7524c8da312 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 21 Oct 2024 10:37:19 -0500 Subject: [PATCH 191/811] Fix bad code quote markdown --- docs/export-map.rst | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/docs/export-map.rst b/docs/export-map.rst index 85ce6a5863..8da9fd7422 100644 --- a/docs/export-map.rst +++ b/docs/export-map.rst @@ -109,7 +109,7 @@ JSON DATA ``KEYS`` The tables containing the [number ID] values for different options. - ``"SHAPE": { + "SHAPE": { "-1": "NONE", "0": "EMPTY", "1": "FLOOR", @@ -118,9 +118,8 @@ JSON DATA "4": "WALL", ... "18": "ENDLESS_PIT" - `` - - ``"PLANT": { + + "PLANT": { "0": "SINGLE-GRAIN_WHEAT", "1": "TWO-GRAIN_WHEAT", "2": "SOFT_WHEAT", @@ -129,13 +128,11 @@ JSON DATA "5": "BARLEY", ... "224": "PALM" - `` - ``"AQUIFER": { + "AQUIFER": { "0": "NONE", "1": "LIGHT", "2": "HEAVY" - `` Note - when using the ``materials`` option, you need to pair the [number ID] with the correct ``KEYS`` material table. Generally you use ``tiletype`` From f72204de02d5d1ab0f5924d872ee31cfc432b51b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 15:38:34 +0000 Subject: [PATCH 192/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/export-map.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/export-map.rst b/docs/export-map.rst index 8da9fd7422..5d922554c0 100644 --- a/docs/export-map.rst +++ b/docs/export-map.rst @@ -118,7 +118,7 @@ JSON DATA "4": "WALL", ... "18": "ENDLESS_PIT" - + "PLANT": { "0": "SINGLE-GRAIN_WHEAT", "1": "TWO-GRAIN_WHEAT", From 56c04f0da12c581bff68fda0e8cd1783000ebe39 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 21 Oct 2024 11:02:49 -0500 Subject: [PATCH 193/811] Fix markdown newlines formatting --- docs/export-map.rst | 46 +++++++++++++++++++-------------------------- 1 file changed, 19 insertions(+), 27 deletions(-) diff --git a/docs/export-map.rst b/docs/export-map.rst index 8da9fd7422..04e1f2c1b0 100644 --- a/docs/export-map.rst +++ b/docs/export-map.rst @@ -94,12 +94,16 @@ JSON DATA ``ARGUMENT_OPTION_ORDER`` The order of the selected options for how data is arranged at a map position + Example 1: - ``{"material": 1, "shape": 2, "hidden": 3}`` - ``map[z][y][x] = {material_data, shape_data, hidden_data}`` + ``{"material": 1, "shape": 2, "hidden": 3}`` + + ``map[z][y][x] = {material_data, shape_data, hidden_data}`` + Example 2: - ``{"variant": 3, "light": 1, "outside": 2, "aquifer": 4}`` - ``map[z][y][x] = {light_data, outside_data, variant_data, aquifer_data}`` + ``{"variant": 3, "light": 1, "outside": 2, "aquifer": 4}`` + + ``map[z][y][x] = {light_data, outside_data, variant_data, aquifer_data}`` ``MAP_SIZE`` A table containing basic information about the map size for width, height, @@ -109,30 +113,14 @@ JSON DATA ``KEYS`` The tables containing the [number ID] values for different options. - "SHAPE": { - "-1": "NONE", - "0": "EMPTY", - "1": "FLOOR", - "2": "BOULDERS", - "3": "PEBBLES", - "4": "WALL", - ... - "18": "ENDLESS_PIT" + ``"SHAPE": {"-1": "NONE", "0": "EMPTY", "1": "FLOOR", "2": "BOULDERS", + "3": "PEBBLES", "4": "WALL", ..., "18": "ENDLESS_PIT"}`` - "PLANT": { - "0": "SINGLE-GRAIN_WHEAT", - "1": "TWO-GRAIN_WHEAT", - "2": "SOFT_WHEAT", - "3": "HARD_WHEAT", - "4": "SPELT", - "5": "BARLEY", - ... - "224": "PALM" - - "AQUIFER": { - "0": "NONE", - "1": "LIGHT", - "2": "HEAVY" + ``"PLANT": {"0": "SINGLE-GRAIN_WHEAT", "1": "TWO-GRAIN_WHEAT", + "2": "SOFT_WHEAT", "3": "HARD_WHEAT", "4": "SPELT", "5": "BARLEY", ..., + "224": "PALM"}`` + + ``"AQUIFER": {"0": "NONE", "1": "LIGHT", "2": "HEAVY"}`` Note - when using the ``materials`` option, you need to pair the [number ID] with the correct ``KEYS`` material table. Generally you use ``tiletype`` @@ -150,7 +138,9 @@ JSON DATA The ``ARGUMENT_OPTION_ORDER`` determines order of tile data. (see above) I would recommend referencing the tile data like so: + ``shape = json_data.map[z][x][y][json_data.ARGUMENT_OPTIONS_ORDER.shape]`` + ``light = json_data.map[z][x][y][json_data.ARGUMENT_OPTIONS_ORDER.light]`` Note - some of the bottom z-levels for hell do not have the same @@ -158,9 +148,11 @@ JSON DATA z-levels are gonna be like 90x90. Instead of returning normal tile data like: + ``map[0][90][90] = {tile_data}`` It will return nil instead: + ``map[0][91][91] = nil`` So you need to account for this! From 7caa91e8e599d4550d80ff6a2b3dabaaed0f05ba Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 21 Oct 2024 16:06:38 +0000 Subject: [PATCH 194/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/export-map.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/export-map.rst b/docs/export-map.rst index 0092500a5c..449187dff4 100644 --- a/docs/export-map.rst +++ b/docs/export-map.rst @@ -27,7 +27,7 @@ Examples Exports the fortress map to JSON with ALL data included ``export-map include -m -s -v`` - Exports the fortress map to JSON with only materials, shape, and variant + Exports the fortress map to JSON with only materials, shape, and variant data included ``export-map exclude --variant --hidden --light`` @@ -113,11 +113,11 @@ JSON DATA ``KEYS`` The tables containing the [number ID] values for different options. - ``"SHAPE": {"-1": "NONE", "0": "EMPTY", "1": "FLOOR", "2": "BOULDERS", + ``"SHAPE": {"-1": "NONE", "0": "EMPTY", "1": "FLOOR", "2": "BOULDERS", "3": "PEBBLES", "4": "WALL", ..., "18": "ENDLESS_PIT"}`` - - ``"PLANT": {"0": "SINGLE-GRAIN_WHEAT", "1": "TWO-GRAIN_WHEAT", - "2": "SOFT_WHEAT", "3": "HARD_WHEAT", "4": "SPELT", "5": "BARLEY", ..., + + ``"PLANT": {"0": "SINGLE-GRAIN_WHEAT", "1": "TWO-GRAIN_WHEAT", + "2": "SOFT_WHEAT", "3": "HARD_WHEAT", "4": "SPELT", "5": "BARLEY", ..., "224": "PALM"}`` ``"AQUIFER": {"0": "NONE", "1": "LIGHT", "2": "HEAVY"}`` From 09c5b267c78b747e27d247c14bb80ba0423a6df6 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 22 Oct 2024 09:38:53 -0500 Subject: [PATCH 195/811] Add evilness option --- docs/export-map.rst | 7 ++++++- export-map.lua | 20 +++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/docs/export-map.rst b/docs/export-map.rst index 449187dff4..11d1b87fbb 100644 --- a/docs/export-map.rst +++ b/docs/export-map.rst @@ -87,7 +87,12 @@ Options TOPAZOLITE/BLACK_OPAL/etc.) (will return nil if the tile is empty) ``-u``, ``--underworld`` - Whether the underworld z-levels will be included [boolean] + Whether the underworld z-levels will be included + +``-e``, ``--evilness`` + Whether the evilness value will be included in MAP_SIZE table. This only + checks the value of the center map tile at ground level and will ignore + biomes at the edges of the map. JSON DATA --------- diff --git a/export-map.lua b/export-map.lua index f0706542e6..4eff32cbf7 100644 --- a/export-map.lua +++ b/export-map.lua @@ -10,6 +10,7 @@ local argparse = require('argparse') local include_underworld_z = false local underworld_z +local evilness -- the layer of the underworld for _, feature in ipairs(df.global.world.features.map_features) do @@ -18,6 +19,21 @@ for _, feature in ipairs(df.global.world.features.map_features) do end end +-- copied from agitation-rebalance.lua +-- check only one tile at the center of the map at ground lvl +-- (this ignore different biomes on the edges of the map) +local function get_evilness() + -- check around ground level + local lvls_above_ground = world.worldgen.worldgen_parms.levels_above_ground + local ground_z = (world.map.z_count - 2) - lvls_above_ground + local xmax, ymax = dfhack.maps.getTileSize() + local center_x, center_y = math.floor(xmax/2), math.floor(ymax/2) + local rgnX, rgnY = dfhack.maps.getTileBiomeRgn(center_x, center_y, ground_z) + local biome = dfhack.maps.getRegionBiome(rgnX, rgnY) + + return biome and biome.evilness or 0 +end + local function classify_tile(options, x, y, z) -- The last z-levels of hell shrink their x/y size unexpectedly! (ಠ_ಠ) -- if your map is 190x190, the last hell z-levels are gonna be like 90x90 @@ -155,6 +171,7 @@ local function export_all_z_levels(fortress_name, folder, options) -- subtract underworld levels if excluded from options z = include_underworld_z and zmax or (zmax - underworld_z), underworld_z_level = include_underworld_z and underworld_z or nil, + evilness = evilness or nil, } data.KEYS = setup_keys(options) @@ -230,7 +247,8 @@ local positionals = argparse.processArgsGetopt(args, { {'a', 'aquifer', handler=function() options.aquifer = true end}, {'m', 'material', handler=function() options.material = true end}, -- local var since underworld not in ordered option - {'u', 'underworld', handler= function() include_underworld_z = true end}, + {'u', 'underworld', handler=function() include_underworld_z = true end}, + {'e', 'evilness', handler=function() evilness = get_evilness() end}, }) if positionals[1] == "help" or options.help then From b2ec8837bfcced3240d35774eb28e78720286254 Mon Sep 17 00:00:00 2001 From: Tim Date: Tue, 22 Oct 2024 10:30:20 -0500 Subject: [PATCH 196/811] Fix global world var --- export-map.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/export-map.lua b/export-map.lua index 4eff32cbf7..16691f8a40 100644 --- a/export-map.lua +++ b/export-map.lua @@ -21,11 +21,13 @@ end -- copied from agitation-rebalance.lua -- check only one tile at the center of the map at ground lvl --- (this ignore different biomes on the edges of the map) +-- (this ignores different biomes on the edges of the map) local function get_evilness() -- check around ground level - local lvls_above_ground = world.worldgen.worldgen_parms.levels_above_ground - local ground_z = (world.map.z_count - 2) - lvls_above_ground + + local lvls_above + lvls_above = df.global.world.worldgen.worldgen_parms.levels_above_ground + local ground_z = (df.global.world.map.z_count - 2) - lvls_above local xmax, ymax = dfhack.maps.getTileSize() local center_x, center_y = math.floor(xmax/2), math.floor(ymax/2) local rgnX, rgnY = dfhack.maps.getTileBiomeRgn(center_x, center_y, ground_z) @@ -183,7 +185,7 @@ local function export_all_z_levels(fortress_name, folder, options) end -- start from bottom z-level (underworld) to top z-level (sky) - for z = zmin, zmax-1 do + for z = 0, 1-1 do local level_data = {} for y = 0, ymax - 1 do local row_data = {} From dd6d9edad1c96aeee0b3679d9a2ff747b8f8b7b3 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 4 Nov 2024 21:49:34 +0000 Subject: [PATCH 197/811] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.29.3 → 0.29.4](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.3...0.29.4) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d93eae187c..f386810993 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.3 + rev: 0.29.4 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 114f4e4b4b424203ca26198a145c9791ece77df9 Mon Sep 17 00:00:00 2001 From: nickthetinker Date: Wed, 6 Nov 2024 08:11:41 -0600 Subject: [PATCH 198/811] Update build.lua related to issue #5012 Quickfort blueprints can't place bridges over stairs The previous code is more restrictive than vanilla UI. In-game, bridges may be placed in any supported position with a walkable, adjacent tile, including over any kind of stair. In-game, floor hatches, grates, and bars do not require an adjacent floor for placement. Initially I thought these changes may be too simple to be correct, but upon further study and testing, I can find no issues. These changes are in keeping with the 'mission statement' from the original dev found in the top comment of the build.ua file: "In general, we enforce the same rules as the in-game UI for allowed placement of buildings (e.g. beds have to be inside, doors have to be adjacent to a wall, etc.). A notable exception is that we allow constructions and machine components to be designated regardless of whether they are reachable or currently supported. This allows the user to designate an entire floor of an above-ground building or an entire power system without micromanagement." --- internal/quickfort/build.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/quickfort/build.lua b/internal/quickfort/build.lua index e2f65ad703..fa053043f7 100644 --- a/internal/quickfort/build.lua +++ b/internal/quickfort/build.lua @@ -139,7 +139,7 @@ local function is_valid_tile_bridge(pos, db_entry, b) (dir == T_direction.Right and pos.x == b.pos.x+b.width-1) then return is_valid_tile_has_space(pos) end - return is_valid_tile_has_space_or_is_ramp(pos) + return is_valid_tile_machine(pos) end -- although vanilla allows constructions to be built on top of constructed @@ -213,7 +213,7 @@ local function is_tile_coverable(pos) shape ~= df.tiletype_shape.STAIR_DOWN) then return false end - return is_tile_floor_adjacent(pos) + return true end -- From d62447adef1a18ca637379fa3ce1d2d691150e09 Mon Sep 17 00:00:00 2001 From: Robob27 Date: Sun, 10 Nov 2024 20:15:53 -0500 Subject: [PATCH 199/811] Fix confirm hover instructions --- changelog.txt | 1 + internal/confirm/specs.lua | 16 ++++++++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index 3809982b04..d3da1c86dc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -38,6 +38,7 @@ Template for new versions: - `makeown`: halt any hostile jobs the unit may be engaged in, like kidnapping - `fix/loyaltycascade`: allow the fix to work on non-dwarven citizens - `control-panel`: fix setting numeric preferences from the commandline +- `gui/confirm`: fix some confirm prompts not working ## Misc Improvements - `control-panel`: Add realistic-melting tweak to control-panel registry diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index a6e0301eb4..b5e356bf72 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -213,7 +213,7 @@ ConfirmSpec{ message='Are you sure you want to delete this route?', intercept_keys='_MOUSE_L', context='dwarfmode/Hauling', - predicate=function() return mi.current_hover == df.main_hover_instruction.RouteRemove end, + predicate=function() return mi.current_hover == df.main_hover_instruction.HAULING_REMOVE_ROUTE end, pausable=true, } @@ -223,7 +223,7 @@ ConfirmSpec{ message='Are you sure you want to delete this stop?', intercept_keys='_MOUSE_L', context='dwarfmode/Hauling', - predicate=function() return mi.current_hover == df.main_hover_instruction.StopRemove end, + predicate=function() return mi.current_hover == df.main_hover_instruction.HAULING_REMOVE_STOP end, pausable=true, } @@ -234,7 +234,7 @@ ConfirmSpec{ intercept_keys='_MOUSE_L', context='dwarfmode/ViewSheets/BUILDING/TradeDepot', predicate=function() - return mi.current_hover == df.main_hover_instruction.BuildingRemove and has_caravans() + return mi.current_hover == df.main_hover_instruction.BUILDING_SHEET_REMOVE and has_caravans() end, } @@ -244,7 +244,7 @@ ConfirmSpec{ message='Are you sure you want to disband this squad?', intercept_keys='_MOUSE_L', context='dwarfmode/Squads', - predicate=function() return mi.current_hover == df.main_hover_instruction.SquadDisband end, + predicate=function() return mi.current_hover == df.main_hover_instruction.SQUAD_DISBAND end, pausable=true, } @@ -438,7 +438,7 @@ ConfirmSpec{ message='Are you sure you want to remove this manager order?', intercept_keys='_MOUSE_L', context='dwarfmode/Info/WORK_ORDERS/Default', - predicate=function() return mi.current_hover == df.main_hover_instruction.ManagerOrderRemove end, + predicate=function() return mi.current_hover == df.main_hover_instruction.WORK_ORDERS_REMOVE end, pausable=true, } @@ -460,8 +460,8 @@ ConfirmSpec{ intercept_keys='_MOUSE_L', context='dwarfmode/Burrow', predicate=function() - return mi.current_hover == df.main_hover_instruction.BurrowRemove or - mi.current_hover == df.main_hover_instruction.BurrowRemovePaint + return mi.current_hover == df.main_hover_instruction.BURROW_REMOVE_EXISTING or + mi.current_hover == df.main_hover_instruction.BURROW_PAINT_REMOVE end, pausable=true, } @@ -472,7 +472,7 @@ ConfirmSpec{ message='Are you sure you want to remove this stockpile?', intercept_keys='_MOUSE_L', context='dwarfmode/Stockpile', - predicate=function() return mi.current_hover == df.main_hover_instruction.StockpileRemove end, + predicate=function() return mi.current_hover == df.main_hover_instruction.STOCKPILE_REMOVE_EXISTING end, pausable=true, } From d6785f34991a48eef279c7d27b9380b2b78d880f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 10 Nov 2024 20:26:01 -0800 Subject: [PATCH 200/811] migrate main_hover_instruction identifiers to new names --- gui/notify.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/notify.lua b/gui/notify.lua index 39bafe93c0..cc469d1480 100644 --- a/gui/notify.lua +++ b/gui/notify.lua @@ -132,14 +132,14 @@ DwarfNotifyOverlay.ATTRS{ } local DWARFMODE_CONFLICTING_TOOLTIPS = utils.invert{ - df.main_hover_instruction.InfoUnits, - df.main_hover_instruction.InfoJobs, - df.main_hover_instruction.InfoPlaces, - df.main_hover_instruction.InfoLabors, - df.main_hover_instruction.InfoWorkOrders, - df.main_hover_instruction.InfoNobles, - df.main_hover_instruction.InfoObjects, - df.main_hover_instruction.InfoJustice, + df.main_hover_instruction.MAIN_OPEN_CREATURES, + df.main_hover_instruction.MAIN_OPEN_TASKS, + df.main_hover_instruction.MAIN_OPEN_PLACES, + df.main_hover_instruction.MAIN_OPEN_LABOR, + df.main_hover_instruction.MAIN_OPEN_WORK_ORDERS, + df.main_hover_instruction.MAIN_OPEN_NOBLES, + df.main_hover_instruction.MAIN_OPEN_OBJECTS, + df.main_hover_instruction.MAIN_OPEN_JUSTICE, } local mi = df.global.game.main_interface From 7078644b3fdb397e15e5be3ccbc90b6ed14f33bf Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 10 Nov 2024 20:28:27 -0800 Subject: [PATCH 201/811] Update changelog.txt --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index d3da1c86dc..3809982b04 100644 --- a/changelog.txt +++ b/changelog.txt @@ -38,7 +38,6 @@ Template for new versions: - `makeown`: halt any hostile jobs the unit may be engaged in, like kidnapping - `fix/loyaltycascade`: allow the fix to work on non-dwarven citizens - `control-panel`: fix setting numeric preferences from the commandline -- `gui/confirm`: fix some confirm prompts not working ## Misc Improvements - `control-panel`: Add realistic-melting tweak to control-panel registry From 6050ded0568520792cf9e96cb758a23392a16184 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 10 Nov 2024 20:33:18 -0800 Subject: [PATCH 202/811] remove obsolete help text --- geld.lua | 23 +---------------------- ungeld.lua | 19 ++----------------- 2 files changed, 3 insertions(+), 39 deletions(-) diff --git a/geld.lua b/geld.lua index 690d6469df..ff50444494 100644 --- a/geld.lua +++ b/geld.lua @@ -1,6 +1,3 @@ --- Gelds or ungelds animals --- Written by Josh Cooper(cppcooper) on 2019-12-10, last modified: 2020-02-23 - utils = require('utils') local validArgs = utils.invert({ @@ -11,29 +8,11 @@ local validArgs = utils.invert({ 'find', }) local args = utils.processArgs({...}, validArgs) -local help = [====[ - -geld -==== -Geld allows the user to geld and ungeld animals. - -Valid options: - -``-unit ``: Gelds the unit with the specified ID. - This is optional; if not specified, the selected unit is used instead. - -``-ungeld``: Ungelds the specified unit instead (see also `ungeld`). - -``-toggle``: Toggles the gelded status of the specified unit. - -``-help``: Shows this help information - -]====] unit=nil if args.help then - print(help) + print(dfhack.script_help()) return end diff --git a/ungeld.lua b/ungeld.lua index b3736974d6..0ea29c2a4e 100644 --- a/ungeld.lua +++ b/ungeld.lua @@ -1,28 +1,13 @@ --- Ungelds animals --- Written by Josh Cooper(cppcooper) on 2019-12-10, last modified: 2020-02-23 utils = require('utils') + local validArgs = utils.invert({ 'unit', 'help', }) local args = utils.processArgs({...}, validArgs) -local help = [====[ - -ungeld -====== -A wrapper around `geld` that ungelds the specified animal. - -Valid options: - -``-unit ``: Ungelds the unit with the specified ID. - This is optional; if not specified, the selected unit is used instead. - -``-help``: Shows this help information - -]====] if args.help then - print(help) + print(dfhack.script_help()) return end From 6e364a70a394c24e268399948ca9b4f8003409b5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 10 Nov 2024 21:01:07 -0800 Subject: [PATCH 203/811] remove gui/manipulator overlay hotkey hint while in dark launch --- gui/manipulator.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 2e2c901d03..b3d2edcba2 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -1341,9 +1341,12 @@ function ManipulatorOverlay:init() } end -OVERLAY_WIDGETS = { - launcher=ManipulatorOverlay, -} +-- +-- disable overlay widget while tool is still in dark launch mode +-- +-- OVERLAY_WIDGETS = { +-- launcher=ManipulatorOverlay, +-- } if dfhack_flags.module then return end From 6e1840e1b5a3dec1e842ccc3bc498ccb49626d0e Mon Sep 17 00:00:00 2001 From: nickthetinker Date: Mon, 11 Nov 2024 19:54:00 -0600 Subject: [PATCH 204/811] Update changelog.txt related to PR #1332 Added line to changelog related to PR #1332 - `gui/quickfort`: fix build mode evluation rules to allow placement of various furniture and constructions on tiles with stair shapes or without orthagonal floor. --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 3809982b04..0ae5625f33 100644 --- a/changelog.txt +++ b/changelog.txt @@ -38,6 +38,7 @@ Template for new versions: - `makeown`: halt any hostile jobs the unit may be engaged in, like kidnapping - `fix/loyaltycascade`: allow the fix to work on non-dwarven citizens - `control-panel`: fix setting numeric preferences from the commandline +- `gui/quickfort`: fix build mode evluation rules to allow placement of various furniture and constructions on tiles with stair shapes or without orthagonal floor. ## Misc Improvements - `control-panel`: Add realistic-melting tweak to control-panel registry From 4e9331ca80ac69581681cb4ab6f63384ae330f62 Mon Sep 17 00:00:00 2001 From: Myk Date: Tue, 12 Nov 2024 03:14:07 -0800 Subject: [PATCH 205/811] doc formatting --- docs/immortal-cravings.rst | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/immortal-cravings.rst b/docs/immortal-cravings.rst index 2ef43c7209..dcb3cb13d5 100644 --- a/docs/immortal-cravings.rst +++ b/docs/immortal-cravings.rst @@ -14,4 +14,6 @@ occupied. Usage ----- -``enable immortal-cravings`` +:: + + enable immortal-cravings From edc7d40c009581e637c39265eb89d7ea149da7da Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 12 Nov 2024 03:18:24 -0800 Subject: [PATCH 206/811] add immortal-cravings to control panel registry --- internal/control-panel/registry.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index c71a53f072..548057b326 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -119,6 +119,7 @@ COMMANDS_BY_IDX = { {command='fastdwarf', group='gameplay', mode='enable'}, {command='hermit', group='gameplay', mode='enable'}, {command='hide-tutorials', group='gameplay', mode='system_enable'}, + {command='immortal-cravings', group='gameplay', mode='enable'}, {command='light-aquifers-only', group='gameplay', mode='run'}, {command='misery', group='gameplay', mode='enable'}, {command='orders-reevaluate', help_command='orders', group='gameplay', mode='repeat', From 782ea3da1654d3fb8c998087a83a383ed26c7e5c Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Thu, 20 Jun 2024 18:18:16 +0100 Subject: [PATCH 207/811] added script commute-sentence to commute the prison sentence of a selected unit --- commute-sentence.lua | 19 +++++++++++++++++++ docs/commute-sentence.rst | 16 ++++++++++++++++ 2 files changed, 35 insertions(+) create mode 100644 commute-sentence.lua create mode 100644 docs/commute-sentence.rst diff --git a/commute-sentence.lua b/commute-sentence.lua new file mode 100644 index 0000000000..8e2a15cb93 --- /dev/null +++ b/commute-sentence.lua @@ -0,0 +1,19 @@ +local utils = require('utils') +local argparse = require('argparse') + +local function commute_sentence(unit) + for _,punishment in ipairs(df.global.plotinfo.punishments) do + if punishment.criminal == unit.id then + punishment.prison_counter = 0 + return + end + end + qerror('Unit is not currently serving a sentence!') +end + +unit = dfhack.gui.getSelectedUnit(true) +if not unit then + qerror('No unit selected!') +else + commute_sentence(unit) +end diff --git a/docs/commute-sentence.rst b/docs/commute-sentence.rst new file mode 100644 index 0000000000..f2f36bbaa1 --- /dev/null +++ b/docs/commute-sentence.rst @@ -0,0 +1,16 @@ +commute-sentence +================ + +.. dfhack-tool:: + :summary: Commute the prison sentences of convicted criminals. + :tags: fort armok units + +If a unit is currently serving out their sentence but you want them released +for whatever reason, this tool can commute their sentence. Just select the unit +and run the command. + +usage +----- + +:: + commute-sentence From 613c3ecaef2a0909b7273aa686b42d3e558b076e Mon Sep 17 00:00:00 2001 From: Najeeb Al-Shabibi Date: Fri, 21 Jun 2024 23:17:56 +0100 Subject: [PATCH 208/811] changed commute-sentence to justice with command option pardon --- commute-sentence.lua | 19 ------------------- docs/commute-sentence.rst | 16 ---------------- docs/justice.rst | 25 ++++++++++++++++++++++++ justice.lua | 40 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 65 insertions(+), 35 deletions(-) delete mode 100644 commute-sentence.lua delete mode 100644 docs/commute-sentence.rst create mode 100644 docs/justice.rst create mode 100644 justice.lua diff --git a/commute-sentence.lua b/commute-sentence.lua deleted file mode 100644 index 8e2a15cb93..0000000000 --- a/commute-sentence.lua +++ /dev/null @@ -1,19 +0,0 @@ -local utils = require('utils') -local argparse = require('argparse') - -local function commute_sentence(unit) - for _,punishment in ipairs(df.global.plotinfo.punishments) do - if punishment.criminal == unit.id then - punishment.prison_counter = 0 - return - end - end - qerror('Unit is not currently serving a sentence!') -end - -unit = dfhack.gui.getSelectedUnit(true) -if not unit then - qerror('No unit selected!') -else - commute_sentence(unit) -end diff --git a/docs/commute-sentence.rst b/docs/commute-sentence.rst deleted file mode 100644 index f2f36bbaa1..0000000000 --- a/docs/commute-sentence.rst +++ /dev/null @@ -1,16 +0,0 @@ -commute-sentence -================ - -.. dfhack-tool:: - :summary: Commute the prison sentences of convicted criminals. - :tags: fort armok units - -If a unit is currently serving out their sentence but you want them released -for whatever reason, this tool can commute their sentence. Just select the unit -and run the command. - -usage ------ - -:: - commute-sentence diff --git a/docs/justice.rst b/docs/justice.rst new file mode 100644 index 0000000000..4334f5057b --- /dev/null +++ b/docs/justice.rst @@ -0,0 +1,25 @@ +justice +======= + +.. dfhack-tool:: + :summary: Commands related to the justice system + :tags: fort armok units + +This tool allows control over aspects of the justice system, such as the +ability to pardon criminals. + +usage +----- + +:: + justice pardon [--unit ] + +Pardon the selected unit or the one specified by unit id if provided. Currently +only applies to prison time and doesn't cancel beatings or hammerings. + + +options +------- + +``-u``, ``--unit `` + Specifies the unit id of the target of the command. diff --git a/justice.lua b/justice.lua new file mode 100644 index 0000000000..f86b4fbed4 --- /dev/null +++ b/justice.lua @@ -0,0 +1,40 @@ + +local argparse = require('argparse') + +local function pardon_unit(unit) + for _,punishment in ipairs(df.global.plotinfo.punishments) do + if punishment.criminal == unit.id then + punishment.prison_counter = 0 + return + end + end + qerror('Unit is not currently serving a sentence!') +end + +local function command_pardon(unit_id) + local unit = nil + if not unit_id then + unit = dfhack.gui.getSelectedUnit() + if not unit then qerror("No unit selected!") end + else + unit = df.unit.find(unit_id) + if not unit then qerror(("No unit with id %i"):format(unit_id)) end + end + if unit then pardon_unit(unit) end +end + +local unit_id = nil + +local args = {...} + +local positionals = argparse.processArgsGetopt(args, + {'u', 'unit', hasArg=true, handler=function(optarg) unit_id = optarg end} +) + +local command = positionals[1] + +if command == "pardon" then + command_pardon(unit_id) +end + +qerror(("Unrecognised command: %s"):format(command)) From 622b7118772a5cdadb1990eba88d402e0233cd6a Mon Sep 17 00:00:00 2001 From: master-spike Date: Mon, 28 Oct 2024 00:27:58 +0000 Subject: [PATCH 209/811] `justice` - line added to changelog for introduction of this script --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 384f2560ad..e95538a05c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools - `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed. - `immortal-cravings`: allow immortals to satisfy their cravings for food and drink +- `justice`: various functions pertaining to the justice system, currently with a command to pardon a unit's prison sentence. ## New Features - `force`: support the ``Wildlife`` event to allow additional wildlife to enter the map From 7d7baca9332d15936ed7a0a05e68edd97f5252b3 Mon Sep 17 00:00:00 2001 From: master-spike Date: Mon, 28 Oct 2024 00:40:59 +0000 Subject: [PATCH 210/811] justice: implement code review suggestions --- docs/justice.rst | 11 ++++++----- justice.lua | 6 ++++-- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/justice.rst b/docs/justice.rst index 4334f5057b..e5ff93cbcb 100644 --- a/docs/justice.rst +++ b/docs/justice.rst @@ -2,23 +2,24 @@ justice ======= .. dfhack-tool:: - :summary: Commands related to the justice system + :summary: Commands related to the justice system. :tags: fort armok units This tool allows control over aspects of the justice system, such as the ability to pardon criminals. -usage +Usage ----- :: justice pardon [--unit ] -Pardon the selected unit or the one specified by unit id if provided. Currently -only applies to prison time and doesn't cancel beatings or hammerings. +Pardon the selected unit or the one specified by unit id (if provided). +Currently only applies to prison time and doesn't cancel beatings or +hammerings. -options +Options ------- ``-u``, ``--unit `` diff --git a/justice.lua b/justice.lua index f86b4fbed4..5071f24458 100644 --- a/justice.lua +++ b/justice.lua @@ -35,6 +35,8 @@ local command = positionals[1] if command == "pardon" then command_pardon(unit_id) +elseif not command then + qerror('Missing command') +else + qerror(("Unrecognised command: %s"):format(command)) end - -qerror(("Unrecognised command: %s"):format(command)) From 536edcfbfe6aa18b2711b638c954e43b6cdbebb7 Mon Sep 17 00:00:00 2001 From: master-spike Date: Sun, 27 Oct 2024 06:14:17 +0000 Subject: [PATCH 211/811] `emigration`: persists last cycle tick so that the behaviour is more consistent with respect to save-and-reloads --- changelog.txt | 1 + emigration.lua | 42 ++++++++++++++++++++++++++++++------------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/changelog.txt b/changelog.txt index 384f2560ad..fc7ee69c19 100644 --- a/changelog.txt +++ b/changelog.txt @@ -40,6 +40,7 @@ Template for new versions: - `fix/loyaltycascade`: allow the fix to work on non-dwarven citizens - `control-panel`: fix setting numeric preferences from the commandline - `gui/quickfort`: fix build mode evluation rules to allow placement of various furniture and constructions on tiles with stair shapes or without orthagonal floor. +- `emigration`: save-and-reload no longer resets the emigration cycle timeout, making gameplay more consistent ## Misc Improvements - `control-panel`: Add realistic-melting tweak to control-panel registry diff --git a/emigration.lua b/emigration.lua index 3721535a3d..1d1dea305e 100644 --- a/emigration.lua +++ b/emigration.lua @@ -1,18 +1,27 @@ --@module = true --@enable = true +local utils = require('utils') + local GLOBAL_KEY = 'emigration' -- used for state change hooks and persistence -enabled = enabled or false +local function get_default_state() + return {enabled=false, last_cycle_tick=0} +end + +state = state or get_default_state() function isEnabled() - return enabled + return state.enabled end local function persist_state() - dfhack.persistent.saveSiteData(GLOBAL_KEY, {enabled=enabled}) + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) end +local TICKS_PER_MONTH = 33600 +local TICKS_PER_YEAR = 12 * TICKS_PER_MONTH + function desireToStay(unit,method,civ_id) -- on a percentage scale local value = 100 - unit.status.current_soul.personality.stress / 5000 @@ -191,18 +200,26 @@ function checkmigrationnow() else for _, civ_id in pairs(merchant_civ_ids) do checkForDeserters('merchant', civ_id) end end + + state.last_cycle_tick = dfhack.world.ReadCurrentTick() + TICKS_PER_YEAR * dfhack.world.ReadCurrentYear() end local function event_loop() - if enabled then - checkmigrationnow() - dfhack.timeout(1, 'months', event_loop) + if state.enabled then + local current_tick = dfhack.world.ReadCurrentTick() + TICKS_PER_YEAR * dfhack.world.ReadCurrentYear() + if current_tick - state.last_cycle_tick < TICKS_PER_MONTH then + local timeout_ticks = state.last_cycle_tick - current_tick + TICKS_PER_MONTH + dfhack.timeout(timeout_ticks, 'ticks', event_loop) + else + checkmigrationnow() + dfhack.timeout(1, 'months', event_loop) + end end end dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_MAP_UNLOADED then - enabled = false + state.enabled = false return end @@ -210,8 +227,9 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) return end - local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {enabled=false}) - enabled = persisted_data.enabled + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) + event_loop() end @@ -230,11 +248,11 @@ if dfhack_flags and dfhack_flags.enable then end if args[1] == "enable" then - enabled = true + state.enabled = true elseif args[1] == "disable" then - enabled = false + state.enabled = false else - print('emigration is ' .. (enabled and 'enabled' or 'not enabled')) + print('emigration is ' .. (state.enabled and 'enabled' or 'not enabled')) return end From 5f3c4024b84b323e1edf50a79084c7061f665e4e Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Fri, 22 Nov 2024 10:37:07 -0500 Subject: [PATCH 212/811] Add infiniteSky to control-panel registry --- internal/control-panel/registry.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 548057b326..888b2bc5cf 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -49,6 +49,8 @@ COMMANDS_BY_IDX = { desc='Go to the Standing Orders tab in the Labor screen to save your current settings.'}, {command='gui/settings-manager load-work-details', group='automation', mode='run', desc='Go to the Work Details tab in the Labor screen to save your current definitions.'}, + {command='infiniteSky', group='automation', mode='enable', + desc='Enable if you want automatic creation of new sky z-levels.'}, {command='logistics enable autoretrain', group='automation', mode='run', desc='Automatically assign trainers to partially trained livestock so they don\'t revert to wild.'}, {command='nestboxes', group='automation', mode='enable'}, From 50e438e22941e4e9bbae2091d5f59b933ee7a21b Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 23 Nov 2024 08:58:17 -0500 Subject: [PATCH 213/811] Adjust to renaming of infiniteSky to infinite-sky --- internal/control-panel/registry.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 888b2bc5cf..61d9763f2b 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -49,7 +49,7 @@ COMMANDS_BY_IDX = { desc='Go to the Standing Orders tab in the Labor screen to save your current settings.'}, {command='gui/settings-manager load-work-details', group='automation', mode='run', desc='Go to the Work Details tab in the Labor screen to save your current definitions.'}, - {command='infiniteSky', group='automation', mode='enable', + {command='infinite-sky', group='automation', mode='enable', desc='Enable if you want automatic creation of new sky z-levels.'}, {command='logistics enable autoretrain', group='automation', mode='run', desc='Automatically assign trainers to partially trained livestock so they don\'t revert to wild.'}, From 157007f0f443c785b760d17c6a970ec6fbc177a1 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Mon, 25 Nov 2024 22:53:13 -0500 Subject: [PATCH 214/811] Update geld to set body_part_status, cleanup implementation --- changelog.txt | 1 + geld.lua | 96 +++++++++++++++++++++++++++++++-------------------- 2 files changed, 60 insertions(+), 37 deletions(-) diff --git a/changelog.txt b/changelog.txt index fc7ee69c19..6e78509017 100644 --- a/changelog.txt +++ b/changelog.txt @@ -41,6 +41,7 @@ Template for new versions: - `control-panel`: fix setting numeric preferences from the commandline - `gui/quickfort`: fix build mode evluation rules to allow placement of various furniture and constructions on tiles with stair shapes or without orthagonal floor. - `emigration`: save-and-reload no longer resets the emigration cycle timeout, making gameplay more consistent +- `geld`/`ungeld`: fix permanence of gelding/ungelding status ## Misc Improvements - `control-panel`: Add realistic-melting tweak to control-panel registry diff --git a/geld.lua b/geld.lua index ff50444494..aada872492 100644 --- a/geld.lua +++ b/geld.lua @@ -5,11 +5,10 @@ local validArgs = utils.invert({ 'toggle', 'ungeld', 'help', - 'find', }) local args = utils.processArgs({...}, validArgs) -unit=nil +local unit = nil if args.help then print(dfhack.script_help()) @@ -21,7 +20,8 @@ if args.unit then if id then unit = df.unit.find(id) else - qerror("Invalid ID provided.") + qerror("Invalid unit ID provided.") + return end else unit = dfhack.gui.getSelectedUnit() @@ -29,58 +29,80 @@ end if not unit then qerror("Invalid unit selection.") + return end if unit.sex == df.pronoun_type.she then - qerror("Cannot geld female animals") + qerror("Cannot geld female animals.") return end -function FindBodyPart(unit,newstate) - bfound = false - for i,wound in ipairs(unit.body.wounds) do - for j,part in ipairs(wound.parts) do - if unit.body.wounds[i].parts[j].flags2.gelded ~= newstate then - bfound = true - if newstate ~= nil then - unit.body.wounds[i].parts[j].flags2.gelded = newstate - end - end +-- Find the geldable body part id, returns -1 on failure +local function FindBodyPartId(unit) + for i,part in ipairs(unit.body.body_plan.body_parts) do + if part.flags.GELDABLE then + return i end end - return bfound + return -1 end -function AddParts(unit) - for i,wound in ipairs(unit.body.wounds) do - if wound.id == 1 and #wound.parts == 0 then - utils.insert_or_update(unit.body.wounds[i].parts,{ new = true, body_part_id = 1 }, 'body_part_id') - end +-- Sets the gelded status of a unit, returns false on failure +local function SetGelded(unit, state) + -- Gelded status is set in a number of places: + -- unit.flags3 + -- unit.body.wounds + -- unit.body.components.body_part_status + + local part_id = FindBodyPartId(unit) + if part_id == -1 then + print("Could not find a geldable body part.") + return false end -end -function Geld(unit) - unit.flags3.gelded = true - if not FindBodyPart(unit,true) then - utils.insert_or_update(unit.body.wounds,{ new = true, id = unit.body.wound_next_id }, 'id') + unit.flags3.gelded = state + + if state then + -- Create new wound + local _,wound,_ = utils.insert_or_update(unit.body.wounds, { new = true, id = unit.body.wound_next_id }, 'id') unit.body.wound_next_id = unit.body.wound_next_id + 1 - AddParts(unit) - if not FindBodyPart(unit,true) then - error("could not find body part") + local _,part,_ = utils.insert_or_update(wound.parts, { new = true, body_part_id = part_id}, 'body_part_id') + part.flags2.gelded = true + else + -- Remove gelding from any existing wounds + for _,wound in ipairs(unit.body.wounds) do + for _,part in ipairs(wound.parts) do + part.flags2.gelded = false + end end end - print(string.format("unit %s gelded.",unit.id)) + + if state then + -- Set part status to gelded + unit.body.components.body_part_status[part_id].gelded = true + else + -- Remove gelded status from all parts + for _,part in ipairs(unit.body.components.body_part_status) do + part.gelded = false + end + end + return true end -function Ungeld(unit) - unit.flags3.gelded = false - FindBodyPart(unit,false) - print(string.format("unit %s ungelded.",unit.id)) +local function Geld(unit) + if SetGelded(unit, true) then + print(string.format("Unit %s gelded.", unit.id)) + else + print(string.format("Failed to geld unit %s.", unit.id)) + end end -if args.find then - print(FindBodyPart(unit) and "found" or "not found") - return +local function Ungeld(unit) + if SetGelded(unit, false) then + print(string.format("Unit %s ungelded.", unit.id)) + else + print(string.format("Failed to ungeld unit %s.", unit.id)) + end end local oldstate = dfhack.units.isGelded(unit) @@ -101,5 +123,5 @@ if newstate ~= oldstate then Ungeld(unit) end else - qerror(string.format("unit %s is already %s", unit.id, oldstate and "gelded" or "ungelded")) + qerror(string.format("Unit %s is already %s.", unit.id, oldstate and "gelded" or "ungelded")) end From 18f48dc488b4a501c3449411ad368c30638d5466 Mon Sep 17 00:00:00 2001 From: Eldresh <32151068+Eldresh@users.noreply.github.com> Date: Tue, 26 Nov 2024 21:37:23 -0600 Subject: [PATCH 215/811] Convert age argument to number in rejuvenate.lua due to the less than comparison, age must be converted to a number or else any attempt to use that argument causes the script to fail with "attempt to compare string with number". --- rejuvenate.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rejuvenate.lua b/rejuvenate.lua index 04c79cdf5f..a2e36c86e2 100644 --- a/rejuvenate.lua +++ b/rejuvenate.lua @@ -84,7 +84,7 @@ local function main(args) table.insert(units, dfhack.gui.getSelectedUnit(true) or qerror("Please select a unit in the UI.")) end for _, u in ipairs(units) do - rejuvenate(u, false, args.force, args['dry-run'], args.age) + rejuvenate(u, false, args.force, args['dry-run'], tonumber(args.age)) end end From dbfa74b8d698d5b16fe9e4af5693b719b671713c Mon Sep 17 00:00:00 2001 From: Eldresh <32151068+Eldresh@users.noreply.github.com> Date: Tue, 26 Nov 2024 23:21:49 -0600 Subject: [PATCH 216/811] Updated changelog.txt with fix to rejuvenate.lua --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index fc7ee69c19..ac0ad32eca 100644 --- a/changelog.txt +++ b/changelog.txt @@ -41,6 +41,7 @@ Template for new versions: - `control-panel`: fix setting numeric preferences from the commandline - `gui/quickfort`: fix build mode evluation rules to allow placement of various furniture and constructions on tiles with stair shapes or without orthagonal floor. - `emigration`: save-and-reload no longer resets the emigration cycle timeout, making gameplay more consistent +- `rejuvenate`: ``--age`` no longer throws the error ``attempt to compare string with number`` ## Misc Improvements - `control-panel`: Add realistic-melting tweak to control-panel registry From bbad80ab63355ba2fc74d8a69c3c855a83147028 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 27 Nov 2024 17:20:19 -0800 Subject: [PATCH 217/811] update spheres ref --- modtools/moddable-gods.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/modtools/moddable-gods.lua b/modtools/moddable-gods.lua index a0f8f76944..5d783f397f 100644 --- a/modtools/moddable-gods.lua +++ b/modtools/moddable-gods.lua @@ -83,7 +83,7 @@ godFig.caste = 0 godFig.sex = gender godFig.name.first_name = args.name for _,sphere in ipairs(args.spheres) do - godFig.info.spheres.spheres:insert('#',df.sphere_type[sphere]) + godFig.info.metaphysical.spheres:insert('#',df.sphere_type[sphere]) end df.global.world.history.figures:insert('#',godFig) From 4062ac8b1d038a119f7a8cdbc2479eea62d173c6 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 28 Nov 2024 10:53:25 -0500 Subject: [PATCH 218/811] Minor cleanup --- geld.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/geld.lua b/geld.lua index aada872492..7b55cdb50f 100644 --- a/geld.lua +++ b/geld.lua @@ -1,4 +1,4 @@ -utils = require('utils') +local utils = require('utils') local validArgs = utils.invert({ 'unit', @@ -21,7 +21,6 @@ if args.unit then unit = df.unit.find(id) else qerror("Invalid unit ID provided.") - return end else unit = dfhack.gui.getSelectedUnit() @@ -29,12 +28,10 @@ end if not unit then qerror("Invalid unit selection.") - return end if unit.sex == df.pronoun_type.she then qerror("Cannot geld female animals.") - return end -- Find the geldable body part id, returns -1 on failure From ebf37dfe62d994598d11d9368919877a52f11f4c Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 28 Nov 2024 11:13:33 -0500 Subject: [PATCH 219/811] Remove uneccessary description --- internal/control-panel/registry.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 61d9763f2b..c162e580bc 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -49,8 +49,7 @@ COMMANDS_BY_IDX = { desc='Go to the Standing Orders tab in the Labor screen to save your current settings.'}, {command='gui/settings-manager load-work-details', group='automation', mode='run', desc='Go to the Work Details tab in the Labor screen to save your current definitions.'}, - {command='infinite-sky', group='automation', mode='enable', - desc='Enable if you want automatic creation of new sky z-levels.'}, + {command='infinite-sky', group='automation', mode='enable'}, {command='logistics enable autoretrain', group='automation', mode='run', desc='Automatically assign trainers to partially trained livestock so they don\'t revert to wild.'}, {command='nestboxes', group='automation', mode='enable'}, From 7b745f61889a7b66c88efec00c1c0eaeec833175 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 28 Nov 2024 09:08:29 -0800 Subject: [PATCH 220/811] add list command and tighten up code --- changelog.txt | 2 +- docs/justice.rst | 12 ++++++++++-- justice.lua | 45 ++++++++++++++++++++++++++++++++------------- 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/changelog.txt b/changelog.txt index c7372e2ad0..9dd52fe75a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,7 +29,7 @@ Template for new versions: ## New Tools - `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed. - `immortal-cravings`: allow immortals to satisfy their cravings for food and drink -- `justice`: various functions pertaining to the justice system, currently with a command to pardon a unit's prison sentence. +- `justice`: various functions pertaining to the justice system, currently with a command to pardon a unit's prison sentence ## New Features - `force`: support the ``Wildlife`` event to allow additional wildlife to enter the map diff --git a/docs/justice.rst b/docs/justice.rst index e5ff93cbcb..ea65a668d2 100644 --- a/docs/justice.rst +++ b/docs/justice.rst @@ -2,7 +2,7 @@ justice ======= .. dfhack-tool:: - :summary: Commands related to the justice system. + :summary: Mess with the justice system. :tags: fort armok units This tool allows control over aspects of the justice system, such as the @@ -12,15 +12,23 @@ Usage ----- :: + justice [list] justice pardon [--unit ] Pardon the selected unit or the one specified by unit id (if provided). Currently only applies to prison time and doesn't cancel beatings or hammerings. +Examples +-------- + +``justice`` + List the convicts currently serving sentences. +``justice pardon`` + Commutes the sentence of the currently selected convict. Options ------- ``-u``, ``--unit `` - Specifies the unit id of the target of the command. + Specifies a specific unit instead of using a selected unit. diff --git a/justice.lua b/justice.lua index 5071f24458..489618e49d 100644 --- a/justice.lua +++ b/justice.lua @@ -1,6 +1,24 @@ - local argparse = require('argparse') +local TICKS_PER_SEASON_TICK = 10 +local TICKS_PER_DAY = 1200 + +local function list_convicts() + local found = false + for _,punishment in ipairs(df.global.plotinfo.punishments) do + local unit = df.unit.find(punishment.criminal) + if unit and punishment.prison_counter > 0 then + found = true + local days = math.ceil((punishment.prison_counter * TICKS_PER_SEASON_TICK) / TICKS_PER_DAY) + print(('%s (id: %d): serving a sentence of %d day(s)'):format( + dfhack.units.getReadableName(unit), unit.id, days)) + end + end + if not found then + print('No criminals currently serving sentences.') + end +end + local function pardon_unit(unit) for _,punishment in ipairs(df.global.plotinfo.punishments) do if punishment.criminal == unit.id then @@ -14,29 +32,30 @@ end local function command_pardon(unit_id) local unit = nil if not unit_id then - unit = dfhack.gui.getSelectedUnit() - if not unit then qerror("No unit selected!") end + unit = dfhack.gui.getSelectedUnit(true) + if not unit then qerror('No unit selected!') end else unit = df.unit.find(unit_id) - if not unit then qerror(("No unit with id %i"):format(unit_id)) end + if not unit then qerror(('No unit with id %d'):format(unit_id)) end end - if unit then pardon_unit(unit) end + pardon_unit(unit) end local unit_id = nil -local args = {...} - -local positionals = argparse.processArgsGetopt(args, - {'u', 'unit', hasArg=true, handler=function(optarg) unit_id = optarg end} +local positionals = argparse.processArgsGetopt({...}, + { + {'u', 'unit', hasArg=true, + handler=function(optarg) unit_id = argparse.nonnegativeInt(optarg, 'unit') end}, + } ) local command = positionals[1] -if command == "pardon" then +if command == 'pardon' then command_pardon(unit_id) -elseif not command then - qerror('Missing command') +elseif not command or command == 'list' then + list_convicts() else - qerror(("Unrecognised command: %s"):format(command)) + qerror(('Unrecognised command: %s'):format(command)) end From ad2ccfae8c974f64909fa15e8f05e888ab75deeb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 28 Nov 2024 11:16:58 -0800 Subject: [PATCH 221/811] update docs, fix review comments --- changelog.txt | 1 + docs/necronomicon.rst | 14 +++++++------- necronomicon.lua | 12 +++++++----- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/changelog.txt b/changelog.txt index 25e1688ee6..7c9a075dd5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -50,6 +50,7 @@ Template for new versions: - `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles - `gui/gm-editor`: automatic display of semantic values for language_name fields - `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. +- `necronomicon`: new ``--world`` option to list all secret-containing items in the entire world ## Removed - `modtools/force`: merged into `force` diff --git a/docs/necronomicon.rst b/docs/necronomicon.rst index 199d04d792..274501327d 100644 --- a/docs/necronomicon.rst +++ b/docs/necronomicon.rst @@ -5,10 +5,10 @@ necronomicon :summary: Find books that contain the secrets of life and death. :tags: fort inspection items -Lists all books in the fortress that contain the secrets to life and death. -To find the books in fortress mode, go to the Written content submenu in -Objects (O). Slabs are not shown by default since dwarves cannot read secrets -from a slab in fort mode. +Lists all books in the fortress (or world) that contain the secrets to life and +death. To zoom to the books in fortress mode, go to the ``Artifacts`` tab in +`gui/sitemap` and click on their names. Slabs are not listed by default since +dwarves cannot read secrets from a slab in fort mode. Usage ----- @@ -24,6 +24,6 @@ Options Also list slabs that contain the secrets of life and death. Note that dwarves cannot read the secrets from a slab in fort mode. -``-w``, ``-world`` - Lists ALL secret containing books and scrolls across the entire world, - not just your fortress. +``-w``, ``--world`` + Lists ALL secret-containing items across the entire world, not just your + fortress. diff --git a/necronomicon.lua b/necronomicon.lua index 91159a48d1..ca760ff4f3 100644 --- a/necronomicon.lua +++ b/necronomicon.lua @@ -9,14 +9,16 @@ function get_book_interactions(item) improvement._type == df.itemimprovement_writingst then for _, content_id in ipairs(improvement.contents) do local written_content = df.written_content.find(content_id) - title = written_content.title + if not written_content then goto continue end + title = written_content.title for _, ref in ipairs (written_content.refs) do if ref._type == df.general_ref_interactionst then local interaction = df.interaction.find(ref.interaction_id) table.insert(book_interactions, interaction) end end + ::continue:: end end end @@ -85,23 +87,23 @@ function necronomicon_world(include_slabs) print() for _,rec in ipairs(df.global.world.artifacts.all) do if df.item_slabst:is_instance(rec.item) and check_slab_secrets(rec.item) then - print(dfhack.TranslateName(rec.name)) + print(dfhack.df2console(dfhack.TranslateName(rec.name))) end end - print() + print() end print("Books and Scrolls:") print() for _,rec in ipairs(df.global.world.artifacts.all) do if df.item_bookst:is_instance(rec.item) or df.item_toolst:is_instance(rec.item) then local title, interactions = get_book_interactions(rec.item) - + if next(interactions) then print(" " .. dfhack.df2console(title)) print_interactions(interactions) print() end - end + end end end From d92a90ed261ef78d7bfbc0adfe2f1d24f88d3d0d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 28 Nov 2024 19:18:49 +0000 Subject: [PATCH 222/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- necronomicon.lua | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/necronomicon.lua b/necronomicon.lua index ca760ff4f3..47d7c374ac 100644 --- a/necronomicon.lua +++ b/necronomicon.lua @@ -82,29 +82,29 @@ function necronomicon(include_slabs) end function necronomicon_world(include_slabs) - if include_slabs then - print("Slabs:") - print() - for _,rec in ipairs(df.global.world.artifacts.all) do - if df.item_slabst:is_instance(rec.item) and check_slab_secrets(rec.item) then - print(dfhack.df2console(dfhack.TranslateName(rec.name))) - end - end - print() - end - print("Books and Scrolls:") + if include_slabs then + print("Slabs:") + print() + for _,rec in ipairs(df.global.world.artifacts.all) do + if df.item_slabst:is_instance(rec.item) and check_slab_secrets(rec.item) then + print(dfhack.df2console(dfhack.TranslateName(rec.name))) + end + end + print() + end + print("Books and Scrolls:") print() for _,rec in ipairs(df.global.world.artifacts.all) do - if df.item_bookst:is_instance(rec.item) or df.item_toolst:is_instance(rec.item) then - local title, interactions = get_book_interactions(rec.item) + if df.item_bookst:is_instance(rec.item) or df.item_toolst:is_instance(rec.item) then + local title, interactions = get_book_interactions(rec.item) - if next(interactions) then - print(" " .. dfhack.df2console(title)) - print_interactions(interactions) - print() - end - end - end + if next(interactions) then + print(" " .. dfhack.df2console(title)) + print_interactions(interactions) + print() + end + end + end end local help = false From e71f21484e8f0c224fb18fc354c9f65c9a66bbe3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Nov 2024 00:59:20 -0800 Subject: [PATCH 223/811] editing pass for changelog --- changelog.txt | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index 7c9a075dd5..156060eab0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,26 +27,25 @@ Template for new versions: # Future ## New Tools -- `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed. +- `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed (you can intentionally stall wildlife incursions by trapping non-caged wildlife in an enclosed area). - `immortal-cravings`: allow immortals to satisfy their cravings for food and drink -- `justice`: various functions pertaining to the justice system, currently with a command to pardon a unit's prison sentence +- `justice`: pardon a criminal's prison sentence ## New Features -- `force`: support the ``Wildlife`` event to allow additional wildlife to enter the map +- `force`: add support a ``Wildlife`` event to allow additional wildlife to enter the map ## Fixes - `gui/quickfort`: only print a help blueprint's text once even if the repeat setting is enabled - `makeown`: quell any active enemy or conflict relationships with converted creatures - `makeown`: halt any hostile jobs the unit may be engaged in, like kidnapping - `fix/loyaltycascade`: allow the fix to work on non-dwarven citizens -- `control-panel`: fix setting numeric preferences from the commandline -- `gui/quickfort`: fix build mode evluation rules to allow placement of various furniture and constructions on tiles with stair shapes or without orthagonal floor. -- `emigration`: save-and-reload no longer resets the emigration cycle timeout, making gameplay more consistent -- `geld`, `ungeld`: fix gelding/ungelding being undone for units who are historical figures when reloading a game +- `control-panel`: fix error when setting numeric preferences from the commandline +- `gui/quickfort`: fix build mode evaluation rules to allow placement of furniture and constructions on tiles with stair shapes or without orthagonal floors +- `emigration`: save-and-reload no longer resets the emigration cycle timeout +- `geld`, `ungeld`: save-and-reload no longer loses changes done by `geld` and `ungeld` for units who are historical figures - `rejuvenate`: fix error when specifying ``--age`` parameter ## Misc Improvements -- `control-panel`: Add realistic-melting tweak to control-panel registry - `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles - `gui/gm-editor`: automatic display of semantic values for language_name fields - `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. From fec52cbac4ce5eedca0967925f9e403a8f32f5a4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Nov 2024 01:17:00 -0800 Subject: [PATCH 224/811] don't vaporize caged or restrained wildlife --- fix/wildlife.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/fix/wildlife.lua b/fix/wildlife.lua index bd80f2433b..336cb701e8 100644 --- a/fix/wildlife.lua +++ b/fix/wildlife.lua @@ -120,6 +120,9 @@ local function unstick_surface_wildlife(opts) if not is_active_wildlife(unit) or unit.animal.leave_countdown > 0 then goto skip end + if unit.flags1.caged or unit.flags1.chained then + goto skip + end if not check_timeout(opts, unit, week_ago_ticks) then goto skip end From b37d2b9e8138ad7b611776e342c08e789fd3956c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 30 Nov 2024 01:36:35 -0800 Subject: [PATCH 225/811] don't mark visitors as hostile unless they are --- changelog.txt | 1 + internal/notify/notifications.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 156060eab0..fd18b433ef 100644 --- a/changelog.txt +++ b/changelog.txt @@ -44,6 +44,7 @@ Template for new versions: - `emigration`: save-and-reload no longer resets the emigration cycle timeout - `geld`, `ungeld`: save-and-reload no longer loses changes done by `geld` and `ungeld` for units who are historical figures - `rejuvenate`: fix error when specifying ``--age`` parameter +- `gui/notify`: don't classify (peacefully) visiting night creatures as hostile ## Misc Improvements - `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index cd3341af62..a0e103fbd2 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -71,6 +71,7 @@ local function for_hostile(fn, reverse) not dfhack.units.isFortControlled(unit) and not dfhack.units.isHidden(unit) and not dfhack.units.isAgitated(unit) and + (not unit.flags2.visitor or unit.flags2.visitor_uninvited) and dfhack.units.isDanger(unit) end, fn, reverse) end From 47ff0f3cb8a88781446b6797d05723f6259dcc09 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 2 Dec 2024 21:53:49 +0000 Subject: [PATCH 226/811] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.29.4 → 0.30.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.29.4...0.30.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f386810993..35ecd16a6b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.4 + rev: 0.30.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 77e9006d39efefe39fb3c50d348158fd1d6dd191 Mon Sep 17 00:00:00 2001 From: nibirubingus <127212797+nibirubingus@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:00:26 +0700 Subject: [PATCH 227/811] Add files via upload --- docs/fixrightclick.rst | 15 +++++++++++++++ fixrightclick.lua | 37 +++++++++++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 docs/fixrightclick.rst create mode 100644 fixrightclick.lua diff --git a/docs/fixrightclick.rst b/docs/fixrightclick.rst new file mode 100644 index 0000000000..abd5e00684 --- /dev/null +++ b/docs/fixrightclick.rst @@ -0,0 +1,15 @@ +fixrightclick +================ + +.. dfhack-tool:: + :summary: Adjust properties of caravans on the map. + :tags: fort interface bugfix + +This overlay changes the behavior of the right mouse button and other keys mapped to "Leave screen" to only reset the selection rectangle when painting designations, constructions, minecart tracks, zones, etc., instead of outright quitting the painting mode. It can be toggled in the UI Overlays tab of `gui/control-panel`. + +Usage +----- + +:: + + overlay enable|disable fixrightclick \ No newline at end of file diff --git a/fixrightclick.lua b/fixrightclick.lua new file mode 100644 index 0000000000..8bad9c1920 --- /dev/null +++ b/fixrightclick.lua @@ -0,0 +1,37 @@ +--@ module = true + +local overlay = require('plugins.overlay') + +RightClickWidget = defclass(RightClickWidget, overlay.OverlayWidget) +RightClickWidget.ATTRS{ + desc='When painting a rectangle, makes right click cancel selection instead of exiting.', + default_enabled=true, + viewscreens={ + 'dwarfmode/Building/Placement', + 'dwarfmode/Designate', + 'dwarfmode/Stockpile/Paint', + 'dwarfmode/Zone/Paint', + 'dwarfmode/Burrow/Paint' + }, +} + +local selection_rect = df.global.selection_rect +local buildreq = df.global.buildreq + +function RightClickWidget:onInput(keys) + if keys._MOUSE_R or keys.LEAVESCREEN then + -- building mode, do not run if buildingplan.planner is enabled since it already provides this functionality + if buildreq.selection_pos.x >= 0 and not overlay.get_state().config['buildingplan.planner'].enabled then + buildreq.selection_pos:clear() + return true + -- all other modes + elseif selection_rect.start_x >= 0 then + selection_rect.start_x = -30000 + selection_rect.start_y = -30000 + selection_rect.start_z = -30000 + return true + end + end +end + +OVERLAY_WIDGETS = {selection=RightClickWidget} \ No newline at end of file From a568a42949bf8c4240c2edd01d73307b5e575d16 Mon Sep 17 00:00:00 2001 From: maksim verkhov <127212797+nibirubingus@users.noreply.github.com> Date: Wed, 4 Dec 2024 17:57:31 +0500 Subject: [PATCH 228/811] Update fixrightclick.rst --- docs/fixrightclick.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/fixrightclick.rst b/docs/fixrightclick.rst index abd5e00684..931ab91f69 100644 --- a/docs/fixrightclick.rst +++ b/docs/fixrightclick.rst @@ -12,4 +12,4 @@ Usage :: - overlay enable|disable fixrightclick \ No newline at end of file + overlay enable|disable fixrightclick.selection From 231b970f13ae212e833fc9d38e1316362cb6237b Mon Sep 17 00:00:00 2001 From: maksim verkhov <127212797+nibirubingus@users.noreply.github.com> Date: Wed, 4 Dec 2024 23:13:21 +0500 Subject: [PATCH 229/811] Delete docs/fixrightclick.rst --- docs/fixrightclick.rst | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 docs/fixrightclick.rst diff --git a/docs/fixrightclick.rst b/docs/fixrightclick.rst deleted file mode 100644 index 931ab91f69..0000000000 --- a/docs/fixrightclick.rst +++ /dev/null @@ -1,15 +0,0 @@ -fixrightclick -================ - -.. dfhack-tool:: - :summary: Adjust properties of caravans on the map. - :tags: fort interface bugfix - -This overlay changes the behavior of the right mouse button and other keys mapped to "Leave screen" to only reset the selection rectangle when painting designations, constructions, minecart tracks, zones, etc., instead of outright quitting the painting mode. It can be toggled in the UI Overlays tab of `gui/control-panel`. - -Usage ------ - -:: - - overlay enable|disable fixrightclick.selection From 643796d11e41479427131126a33336f5f4e144aa Mon Sep 17 00:00:00 2001 From: maksim verkhov <127212797+nibirubingus@users.noreply.github.com> Date: Wed, 4 Dec 2024 23:15:18 +0500 Subject: [PATCH 230/811] Delete fixrightclick.lua --- fixrightclick.lua | 37 ------------------------------------- 1 file changed, 37 deletions(-) delete mode 100644 fixrightclick.lua diff --git a/fixrightclick.lua b/fixrightclick.lua deleted file mode 100644 index 8bad9c1920..0000000000 --- a/fixrightclick.lua +++ /dev/null @@ -1,37 +0,0 @@ ---@ module = true - -local overlay = require('plugins.overlay') - -RightClickWidget = defclass(RightClickWidget, overlay.OverlayWidget) -RightClickWidget.ATTRS{ - desc='When painting a rectangle, makes right click cancel selection instead of exiting.', - default_enabled=true, - viewscreens={ - 'dwarfmode/Building/Placement', - 'dwarfmode/Designate', - 'dwarfmode/Stockpile/Paint', - 'dwarfmode/Zone/Paint', - 'dwarfmode/Burrow/Paint' - }, -} - -local selection_rect = df.global.selection_rect -local buildreq = df.global.buildreq - -function RightClickWidget:onInput(keys) - if keys._MOUSE_R or keys.LEAVESCREEN then - -- building mode, do not run if buildingplan.planner is enabled since it already provides this functionality - if buildreq.selection_pos.x >= 0 and not overlay.get_state().config['buildingplan.planner'].enabled then - buildreq.selection_pos:clear() - return true - -- all other modes - elseif selection_rect.start_x >= 0 then - selection_rect.start_x = -30000 - selection_rect.start_y = -30000 - selection_rect.start_z = -30000 - return true - end - end -end - -OVERLAY_WIDGETS = {selection=RightClickWidget} \ No newline at end of file From a903e829921fab4ad910286da1f0c916d78a0467 Mon Sep 17 00:00:00 2001 From: maksim verkhov <127212797+nibirubingus@users.noreply.github.com> Date: Thu, 5 Dec 2024 00:07:52 +0500 Subject: [PATCH 231/811] Update overlay info in design.rst --- docs/gui/design.rst | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/docs/gui/design.rst b/docs/gui/design.rst index 29d46dfebb..ac6c14e9cc 100644 --- a/docs/gui/design.rst +++ b/docs/gui/design.rst @@ -4,7 +4,7 @@ gui/design .. dfhack-tool:: :summary: Design designation utility with shapes. - :tags: fort design productivity map + :tags: fort design productivity interface map This tool provides a point and click interface to make designating shapes and patterns easier. Supports both digging designations and placing constructions. @@ -19,7 +19,18 @@ Usage Overlay ------- -This script provides an overlay that shows the selected dimensions when -designating something with vanilla tools, for example when painting a burrow or -designating digging. The dimensions show up in a tooltip that follows the mouse -cursor. +This tool also provides two overlays that are managed by the `overlay` framework. + +dimensions +~~~~~~~~~~ + +The ``gui/design.dimensions`` overlay shows the selected dimensions when designating +with vanilla tools, for example when painting a burrow or designating digging. +The dimensions show up in a tooltip that follows the mousecursor. + +rightclick +~~~~~~~~~~ + +The ``gui/design.rightclick`` overlay prevents the right mouse button and other keys +bound to "Leave screen" from exiting out of designation mode when drawing a box with +vanilla tools, instead making it cancel the designation first. From c5a6501779356327a77f1c18c2435e6591110980 Mon Sep 17 00:00:00 2001 From: maksim verkhov <127212797+nibirubingus@users.noreply.github.com> Date: Thu, 5 Dec 2024 00:09:37 +0500 Subject: [PATCH 232/811] add RightClickOverlay --- gui/design.lua | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/gui/design.lua b/gui/design.lua index 5e90a47978..8166a83959 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -120,8 +120,42 @@ function DimensionsOverlay:preUpdateLayout(parent_rect) self.frame.h = parent_rect.height end +--- +--- RightClickOverlay +--- + +RightClickOverlay = defclass(RightClickOverlay, overlay.OverlayWidget) +RightClickOverlay.ATTRS{ + desc='When drawing boxes, makes right click cancel selection instead of exiting.', + default_enabled=true, + viewscreens={ + 'dwarfmode/Designate', + 'dwarfmode/Burrow/Paint', + 'dwarfmode/Stockpile/Paint', + 'dwarfmode/Zone/Paint', + 'dwarfmode/Building/Placement' + }, +} + +function RightClickOverlay:onInput(keys) + if keys._MOUSE_R or keys.LEAVESCREEN then + -- building mode + if uibs.selection_pos.x >= 0 then + uibs.selection_pos:clear() + return true + -- all other modes + elseif selection_rect.start_x >= 0 then + selection_rect.start_x = -30000 + selection_rect.start_y = -30000 + selection_rect.start_z = -30000 + return true + end + end +end + OVERLAY_WIDGETS = { dimensions=DimensionsOverlay, + rightclick=RightClickOverlay } --- From 946cf3a4dd1acfbe918f6003752ec0146be72b9c Mon Sep 17 00:00:00 2001 From: maksim verkhov <127212797+nibirubingus@users.noreply.github.com> Date: Thu, 5 Dec 2024 01:15:33 +0500 Subject: [PATCH 233/811] `gui/design`: new overlay ``gui/design.rightclick`` --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index fd18b433ef..93ead4a436 100644 --- a/changelog.txt +++ b/changelog.txt @@ -51,6 +51,7 @@ Template for new versions: - `gui/gm-editor`: automatic display of semantic values for language_name fields - `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. - `necronomicon`: new ``--world`` option to list all secret-containing items in the entire world +- `gui/design`: new overlay ``gui/design.rightclick`` that prevents right click from closing designation mode when drawing rectangles ## Removed - `modtools/force`: merged into `force` From 0f1eef52caf2b19fd7d987582e489007b3afa154 Mon Sep 17 00:00:00 2001 From: maksim verkhov <127212797+nibirubingus@users.noreply.github.com> Date: Thu, 5 Dec 2024 01:24:46 +0500 Subject: [PATCH 234/811] slightly better wording on ``gui/design.rightclick`` --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 93ead4a436..771ba18df5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -51,7 +51,7 @@ Template for new versions: - `gui/gm-editor`: automatic display of semantic values for language_name fields - `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. - `necronomicon`: new ``--world`` option to list all secret-containing items in the entire world -- `gui/design`: new overlay ``gui/design.rightclick`` that prevents right click from closing designation mode when drawing rectangles +- `gui/design`: new ``gui/design.rightclick`` overlay that prevents right click from closing designation mode when drawing boxes and minecart tracks ## Removed - `modtools/force`: merged into `force` From d288642b3ca7006b7f5bfa299df5b5720082278b Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 5 Dec 2024 15:48:23 -0500 Subject: [PATCH 235/811] Reset racefilter on world and map load Prevents usage of stale state when loading multiple worlds or timelines in a single session. --- exportlegends.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/exportlegends.lua b/exportlegends.lua index 80a4ec2996..305c285451 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -1038,6 +1038,11 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_VIEWSCREEN_CHANGED and df.viewscreen_choose_game_typest:is_instance(dfhack.gui.getDFViewscreen(true)) then asyncexport.reset_state() end + + -- Reset state when a world or map is loaded to ensure data remains current + if sc == SC_WORLD_LOADED or sc == SC_MAP_LOADED then + racefilter.reset_state() + end end if dfhack_flags.module then From e8bb513f12baa6dde8c7efd40ae770fc85380b8e Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 5 Dec 2024 20:46:11 -0500 Subject: [PATCH 236/811] Add entry to changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index fd18b433ef..6bb14ce5d9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -45,6 +45,7 @@ Template for new versions: - `geld`, `ungeld`: save-and-reload no longer loses changes done by `geld` and `ungeld` for units who are historical figures - `rejuvenate`: fix error when specifying ``--age`` parameter - `gui/notify`: don't classify (peacefully) visiting night creatures as hostile +- `exportlegends`: fix race filter not refreshing on world load, leading to incorrect results ## Misc Improvements - `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles From 0beb896a89f70f00215964b2feb5d7e130277233 Mon Sep 17 00:00:00 2001 From: Myk Date: Thu, 5 Dec 2024 18:46:36 -0800 Subject: [PATCH 237/811] Apply suggestions from code review --- docs/gui/design.rst | 2 +- gui/design.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/gui/design.rst b/docs/gui/design.rst index ac6c14e9cc..5518af6c9c 100644 --- a/docs/gui/design.rst +++ b/docs/gui/design.rst @@ -26,7 +26,7 @@ dimensions The ``gui/design.dimensions`` overlay shows the selected dimensions when designating with vanilla tools, for example when painting a burrow or designating digging. -The dimensions show up in a tooltip that follows the mousecursor. +The dimensions show up in a tooltip that follows the mouse cursor. rightclick ~~~~~~~~~~ diff --git a/gui/design.lua b/gui/design.lua index 8166a83959..dac68bd5f3 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -155,7 +155,7 @@ end OVERLAY_WIDGETS = { dimensions=DimensionsOverlay, - rightclick=RightClickOverlay + rightclick=RightClickOverlay, } --- From a21636e77a08705f06ad0ee6f326ab550d1d8780 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 5 Dec 2024 18:49:26 -0800 Subject: [PATCH 238/811] handle zone painting for dims tooltip --- changelog.txt | 1 + gui/design.lua | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index fd18b433ef..22f438e71f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -50,6 +50,7 @@ Template for new versions: - `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles - `gui/gm-editor`: automatic display of semantic values for language_name fields - `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. +- `gui/design`: add dimensions tooltip to vanilla zone painting interface - `necronomicon`: new ``--world`` option to list all secret-containing items in the entire world ## Removed diff --git a/gui/design.lua b/gui/design.lua index 5e90a47978..e011fc8a61 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -67,6 +67,7 @@ DimensionsOverlay.ATTRS{ 'dwarfmode/Designate', 'dwarfmode/Burrow/Paint', 'dwarfmode/Stockpile/Paint', + 'dwarfmode/Zone/Paint', 'dwarfmode/Building/Placement', }, } @@ -101,10 +102,11 @@ function DimensionsOverlay:init() } end --- don't imply that stockpiles will be 3d +-- don't imply that stockpiles or zones will be 3d local function check_stockpile_dims() - if main_interface.bottom_mode_selected == df.main_bottom_mode_type.STOCKPILE_PAINT and - selection_rect.start_x > 0 + if selection_rect.start_x > 0 and + (main_interface.bottom_mode_selected == df.main_bottom_mode_type.STOCKPILE_PAINT or + main_interface.bottom_mode_selected == df.main_bottom_mode_type.ZONE_PAINT) then selection_rect.start_z = df.global.window_z end From 24994be5eb5425a3bba8c7df02041e2d37d9e03e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 5 Dec 2024 18:55:37 -0800 Subject: [PATCH 239/811] make alarm setting and clearing functions public --- gui/civ-alert.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 37bec014de..ebdaa6662a 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -18,21 +18,22 @@ local function get_civ_alert() return list[1] end -local function can_sound_alarm() +-- public API section +function can_sound_alarm() return df.global.plotinfo.alerts.civ_alert_idx == 0 and #get_civ_alert().burrows > 0 end -local function sound_alarm() +function sound_alarm() if not can_sound_alarm() then return end df.global.plotinfo.alerts.civ_alert_idx = 1 end -local function can_clear_alarm() +function can_clear_alarm() return df.global.plotinfo.alerts.civ_alert_idx ~= 0 end -local function clear_alarm() +function clear_alarm() df.global.plotinfo.alerts.civ_alert_idx = 0 end From b6372bf1c7660a859ec1dd58bd20b83d4329a277 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Thu, 5 Dec 2024 23:01:45 -0500 Subject: [PATCH 240/811] Only reset state on world load --- exportlegends.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exportlegends.lua b/exportlegends.lua index 305c285451..db5e12ffb1 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -1040,7 +1040,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) end -- Reset state when a world or map is loaded to ensure data remains current - if sc == SC_WORLD_LOADED or sc == SC_MAP_LOADED then + if sc == SC_WORLD_LOADED then racefilter.reset_state() end end From e888316c3bcb37c5ec39ca4310ce3c6f30ede051 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 6 Dec 2024 22:28:29 -0800 Subject: [PATCH 241/811] revert hostile notification fix now that the underlying issue in isDanger is fixed --- internal/notify/notifications.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index a0e103fbd2..cd3341af62 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -71,7 +71,6 @@ local function for_hostile(fn, reverse) not dfhack.units.isFortControlled(unit) and not dfhack.units.isHidden(unit) and not dfhack.units.isAgitated(unit) and - (not unit.flags2.visitor or unit.flags2.visitor_uninvited) and dfhack.units.isDanger(unit) end, fn, reverse) end From 35a5e011130885973e8a5cfa656fc29d9145707c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 7 Dec 2024 06:05:41 -0800 Subject: [PATCH 242/811] changelog editing pass --- changelog.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 41da50ca88..e7a54c78cd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,12 +27,12 @@ Template for new versions: # Future ## New Tools -- `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed (you can intentionally stall wildlife incursions by trapping non-caged wildlife in an enclosed area). +- `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed (you can intentionally stall wildlife incursions by trapping wildlife in an enclosed area so they are not caged but still cannot escape). - `immortal-cravings`: allow immortals to satisfy their cravings for food and drink - `justice`: pardon a criminal's prison sentence ## New Features -- `force`: add support a ``Wildlife`` event to allow additional wildlife to enter the map +- `force`: add support for a ``Wildlife`` event to allow additional wildlife to enter the map ## Fixes - `gui/quickfort`: only print a help blueprint's text once even if the repeat setting is enabled @@ -45,15 +45,15 @@ Template for new versions: - `geld`, `ungeld`: save-and-reload no longer loses changes done by `geld` and `ungeld` for units who are historical figures - `rejuvenate`: fix error when specifying ``--age`` parameter - `gui/notify`: don't classify (peacefully) visiting night creatures as hostile -- `exportlegends`: fix race filter not refreshing on world load, leading to incorrect results +- `exportlegends`: ensure historical figure race filter is usable after re-entering legends mode with a different loaded world ## Misc Improvements - `idle-crafting`: also support making shell crafts for workshops with linked input stockpiles -- `gui/gm-editor`: automatic display of semantic values for language_name fields +- `gui/gm-editor`: automatically resolve and display names for ``language_name`` fields - `fix/stuck-worship`: reduced console output by default. Added ``--verbose`` and ``--quiet`` options. - `gui/design`: add dimensions tooltip to vanilla zone painting interface - `necronomicon`: new ``--world`` option to list all secret-containing items in the entire world -- `gui/design`: new ``gui/design.rightclick`` overlay that prevents right click from closing designation mode when drawing boxes and minecart tracks +- `gui/design`: new ``gui/design.rightclick`` overlay that allows you to cancel out of partially drawn box and minecart designations without canceling completely out of drawing mode ## Removed - `modtools/force`: merged into `force` From d0d308da36a37a3e05fb7e73ef9a5bf9dd3b0d4b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 8 Dec 2024 17:54:09 -0800 Subject: [PATCH 243/811] update changelog for 50.14-r2 --- changelog.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/changelog.txt b/changelog.txt index e7a54c78cd..aa662860aa 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,18 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 50.14-r2 + ## New Tools - `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed (you can intentionally stall wildlife incursions by trapping wildlife in an enclosed area so they are not caged but still cannot escape). - `immortal-cravings`: allow immortals to satisfy their cravings for food and drink From fca4f0c7abe2e6d01596985cd5fca61c31bea114 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 10 Dec 2024 09:11:14 -0800 Subject: [PATCH 244/811] improve description for realistic-melting tweak --- internal/control-panel/registry.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index c162e580bc..04c374df7e 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -130,7 +130,7 @@ COMMANDS_BY_IDX = { desc='Displays percentages on partially-consumed items like hospital cloth.'}, {command='pop-control', group='gameplay', mode='enable'}, {command='realistic-melting', help_command='tweak', group='gameplay', mode='tweak', - desc='Adjust selected item types melt return for all metals to ~95% of forging cost. Reduce melt return by 10% per wear level.'}, + desc='Fixes metal duplication exploits by setting the melt return for all items to ~95%, reduced by 10% for each wear level.'}, {command='starvingdead', group='gameplay', mode='enable'}, {command='timestream', group='gameplay', mode='enable'}, {command='work-now', group='gameplay', mode='enable'}, From da045a28898cc8d83ef752ad58a5c62338b8babd Mon Sep 17 00:00:00 2001 From: maksim verkhov <127212797+nibirubingus@users.noreply.github.com> Date: Wed, 11 Dec 2024 21:27:13 +0500 Subject: [PATCH 245/811] make `gui/design.rightclick` fullscreen forgot this last time - this removes it from `gui/overlay` --- gui/design.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/design.lua b/gui/design.lua index 7472aaa96a..22666d83fe 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -130,6 +130,7 @@ RightClickOverlay = defclass(RightClickOverlay, overlay.OverlayWidget) RightClickOverlay.ATTRS{ desc='When drawing boxes, makes right click cancel selection instead of exiting.', default_enabled=true, + fullscreen=true, viewscreens={ 'dwarfmode/Designate', 'dwarfmode/Burrow/Paint', From 0a5380c0f653c0910701638608f0de5a8b34420e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 12 Dec 2024 05:43:27 -0800 Subject: [PATCH 246/811] bump changelog version to 50.15-r1 --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index aa662860aa..acae15c380 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,6 +36,8 @@ Template for new versions: ## Removed +# 50.15-r1 + # 50.14-r2 ## New Tools From ecabf5156ab2f772b13f21bf0a097e45753c98d3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 24 Dec 2024 19:39:51 -0800 Subject: [PATCH 247/811] use the new repeat-util API for querying --- internal/control-panel/common.lua | 4 ++-- repeat.lua | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/control-panel/common.lua b/internal/control-panel/common.lua index 922f053644..4cdc23191e 100644 --- a/internal/control-panel/common.lua +++ b/internal/control-panel/common.lua @@ -73,7 +73,7 @@ function get_enabled_map() end end -- repeat entries override tool names for control-panel - for munged_name in pairs(repeatUtil.repeating) do + for _,munged_name in ipairs(repeatUtil.listScheduled()) do local name = unmunge_repeat_name(munged_name) if name then enabled_map[name] = true @@ -118,7 +118,7 @@ local function persist_repeats() local cp_repeats = {} for _, data in ipairs(registry.COMMANDS_BY_IDX) do if data.mode == 'repeat' then - if repeatUtil.repeating[munge_repeat_name(data.command)] then + if repeatUtil.isScheduled(munge_repeat_name(data.command)) then cp_repeats[data.command] = true else cp_repeats[data.command] = false diff --git a/repeat.lua b/repeat.lua index 771d61fafe..ce3ab43e67 100644 --- a/repeat.lua +++ b/repeat.lua @@ -46,8 +46,8 @@ elseif args.command then end if args.list then - for k in pairs(repeatUtil.repeating) do - print(k) + for _,name in ipairs(repeatUtil.listScheduled()) do + print(name) end return end From a98250b6bd6f34b4874784a9171dc7ed205f8f9d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 24 Dec 2024 19:55:43 -0800 Subject: [PATCH 248/811] initial implementation of fix/stuck-squad --- changelog.txt | 1 + docs/fix/stuck-squad.rst | 24 ++++++++++ fix/stuck-squad.lua | 100 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 125 insertions(+) create mode 100644 docs/fix/stuck-squad.rst create mode 100644 fix/stuck-squad.lua diff --git a/changelog.txt b/changelog.txt index acae15c380..796d6c63b7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `fix/stuck-squad`: allow squads returning from missions to rescue other squads that have gotten stuck on the world map ## New Features diff --git a/docs/fix/stuck-squad.rst b/docs/fix/stuck-squad.rst new file mode 100644 index 0000000000..13bfc0bca5 --- /dev/null +++ b/docs/fix/stuck-squad.rst @@ -0,0 +1,24 @@ +fix/stuck-squad +=============== + +.. dfhack-tool:: + :summary: Allow squads returning from missions to rescue lost squads. + :tags: fort bugfix military + +Occasionally, squads that you send out on a mission get stuck on the world map. +They lose their ability to navigate and are unable to return to your fortress. +This tool finds another of your squads that is returning from a mission and +assigns them to rescue the lost squad. + +This fix is enabled by default in the DFHack +`control panel `, or you can run it as needed. However, it +is still up to you to send out another squad that can be tasked with the rescue +mission. You can send the rescue squad out on an innocuous "Demand tribute" +mission to minimize risk to the squad. + +Usage +----- + +:: + + fix/stuck-squad diff --git a/fix/stuck-squad.lua b/fix/stuck-squad.lua new file mode 100644 index 0000000000..987fabbdca --- /dev/null +++ b/fix/stuck-squad.lua @@ -0,0 +1,100 @@ +--@ module=true + +local utils = require('utils') + +-- from observing bugged saves, this condition appears to be unique to stuck armies +local function is_army_stuck(army) + return army.controller_id ~= 0 and not army.controller +end + +-- if army is currently camping, we'll need to go up the chain +local function get_top_controller(controller) + if not controller then return end + if controller.master_id == controller.id then return controller end + return df.army_controller.find(controller.master_id) +end + +local function is_army_valid_and_returning(army) + local controller = get_top_controller(army.controller) + if not controller or controller.goal ~= df.army_controller_goal_type.SITE_INVASION then + return false, false + end + return true, controller.data.goal_site_invasion.flag.RETURNING_HOME +end + +-- need to check all squad positions since some members may have died +local function get_squad_army(squad) + if not squad then return end + for _,sp in ipairs(squad.positions) do + local hf = df.historical_figure.find(sp.occupant) + if not hf then goto continue end + local army = df.army.find(hf.info and hf.info.whereabouts and hf.info.whereabouts.army_id or -1) + if army then return army end + ::continue:: + end +end + +-- called by gui/notify notification +function scan_fort_armies() + local stuck_armies, outbound_army, returning_army = {}, nil, nil + local govt = df.historical_entity.find(df.global.plotinfo.group_id) + if not govt then return stuck_armies, outbound_army, returning_army end + + for _,squad_id in ipairs(govt.squads) do + local squad = df.squad.find(squad_id) + local army = get_squad_army(squad) + if not army then goto continue end + if is_army_stuck(army) then + table.insert(stuck_armies, {squad=squad, army=army}) + elseif not returning_army then + local valid, returning = is_army_valid_and_returning(army) + if valid then + if returning then + returning_army = {squad=squad, army=army} + else + outbound_army = {squad=squad, army=army} + end + end + end + ::continue:: + end + return stuck_armies, outbound_army, returning_army +end + +local function unstick_armies() + local stuck_armies, outbound_army, returning_army = scan_fort_armies() + if #stuck_armies == 0 then return end + if not returning_army then + local instructions = outbound_army + and ('Please wait for %s to complete their objective and run this command again when they are on their way home.'):format( + dfhack.df2console(dfhack.military.getSquadName(outbound_army.squad.id))) + or 'Please send a squad out on a mission that will return to the fort, and'.. + ' run this command again when they are on the way home.' + qerror(('%d stuck arm%s found, but no returning armies found to rescue them!\n%s'):format( + #stuck_armies, #stuck_armies == 1 and 'y' or 'ies', instructions)) + return + end + local returning_squad_name = dfhack.df2console(dfhack.military.getSquadName(returning_army.squad.id)) + for _,stuck in ipairs(stuck_armies) do + print(('fix/stuck-squad: Squad rescue operation underway! %s is rescuing %s'):format( + returning_squad_name, dfhack.military.getSquadName(stuck.squad.id))) + for _,member in ipairs(stuck.army.members) do + local nemesis = df.nemesis_record.find(member.nemesis_id) + if not nemesis or not nemesis.figure then goto continue end + local hf = nemesis.figure + if hf.info and hf.info.whereabouts then + hf.info.whereabouts.army_id = returning_army.army.id + end + utils.insert_sorted(returning_army.army.members, member, 'nemesis_id') + ::continue:: + end + stuck.army.members:resize(0) + utils.insert_sorted(get_top_controller(returning_army.army.controller).assigned_squads, stuck.squad.id) + end +end + +if dfhack_flags.module then + return +end + +unstick_armies() From e64a88e2e99c8813a11b7942eb59c8c744698fff Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 24 Dec 2024 19:55:57 -0800 Subject: [PATCH 249/811] add control panel registry entry for fix/stuck-squad --- internal/control-panel/registry.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 04c374df7e..04b28b3ff5 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -88,8 +88,9 @@ COMMANDS_BY_IDX = { params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/ownership', ']'}}, {command='fix/protect-nicks', group='bugfix', mode='enable', default=true}, {command='fix/stuck-instruments', group='bugfix', mode='repeat', default=true, - desc='Fix activity references on stuck instruments to make them usable again.', params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}}, + {command='fix/stuck-squad', group='bugfix', mode='repeat', default=true, + params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-squad', ']'}}, {command='fix/stuck-worship', group='bugfix', mode='repeat', default=true, params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-worship', '-q', ']'}}, {command='fix/noexert-exhaustion', group='bugfix', mode='repeat', default=true, From cc2dc94599511fc76c8f23aae55c8aef474c4b8b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 24 Dec 2024 19:56:08 -0800 Subject: [PATCH 250/811] add notification for stuck squad and a player needs to take action --- internal/notify/notifications.lua | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index cd3341af62..160f9f8136 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -1,8 +1,11 @@ --@module = true +local dlg = require('gui.dialogs') local gui = require('gui') local json = require('json') local list_agreements = reqscript('list-agreements') +local repeat_util = require('repeat-util') +local stuck_squad = reqscript('fix/stuck-squad') local warn_stranded = reqscript('warn-stranded') local CONFIG_FILE = 'dfhack-config/notify.json' @@ -302,6 +305,34 @@ end -- the order of this list controls the order the notifications will appear in the overlay NOTIFICATIONS_BY_IDX = { + { + name='stuck_squad', + desc='Notifies when a squad is stuck on the world map.', + default=true, + dwarf_fn=function() + local stuck_armies, outbound_army, returning_army = stuck_squad.scan_fort_armies() + if #stuck_armies == 0 then return end + if repeat_util.isScheduled('control-panel/fix/stuck-squad') and (outbound_army or returning_army) then + return + end + return ('%d squad%s need%s rescue'):format( + #stuck_armies, + #stuck_armies == 1 and '' or 's', + #stuck_armies == 1 and 's' or '' + ) + end, + on_click=function() + local message = 'A squad is lost on the world map and needs rescue!\n\n' .. + 'Please send a squad out on a mission that will return to the fort.\n' .. + 'They will rescue the stuck squad on their way home.' + if not repeat_util.isScheduled('control-panel/fix/stuck-squad') then + message = message .. '\n\n' .. + 'Please enable fix/stuck-squad in the DFHack control panel to allow\n'.. + 'the rescue mission to happen.' + end + dlg.showMessage('Rescue stuck squads', message, COLOR_WHITE) + end, + }, { name='traders_ready', desc='Notifies when traders are ready to trade at the depot.', From 741bc62184ebfe79ce89fce701f357d26228b01f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 24 Dec 2024 21:10:57 -0800 Subject: [PATCH 251/811] editing pass for fix/stuck-squad help text --- docs/fix/stuck-squad.rst | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/docs/fix/stuck-squad.rst b/docs/fix/stuck-squad.rst index 13bfc0bca5..1284f82324 100644 --- a/docs/fix/stuck-squad.rst +++ b/docs/fix/stuck-squad.rst @@ -7,14 +7,22 @@ fix/stuck-squad Occasionally, squads that you send out on a mission get stuck on the world map. They lose their ability to navigate and are unable to return to your fortress. -This tool finds another of your squads that is returning from a mission and -assigns them to rescue the lost squad. +This tool allows another of your squads that is (successfully) returning from a +mission to rescue the lost squad along the way and bring them home. This fix is enabled by default in the DFHack `control panel `, or you can run it as needed. However, it is still up to you to send out another squad that can be tasked with the rescue -mission. You can send the rescue squad out on an innocuous "Demand tribute" -mission to minimize risk to the squad. +mission. You can send the squad out on an innocuous "Demand tribute" mission to +minimize risk to the squad. + +This tool is integrated with `gui/notify`, so you will get a notification in +the DFHack notification panel when a squad is stuck and there are no squads +currently out traveling that can rescue them. + +Note that there might be other reasons why your squad appears missing -- if it +got wiped out in combat and nobody survived to report back, for example -- but +this tool should fix the cases that are actual bugs. Usage ----- From 4dd7f61cfa14dd376e028bcd82a13cdde1dd5fcb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 24 Dec 2024 23:49:33 -0800 Subject: [PATCH 252/811] additional editing pass for stuck-squad text --- docs/fix/stuck-squad.rst | 4 ++-- internal/notify/notifications.lua | 7 ++++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/fix/stuck-squad.rst b/docs/fix/stuck-squad.rst index 1284f82324..ec06913955 100644 --- a/docs/fix/stuck-squad.rst +++ b/docs/fix/stuck-squad.rst @@ -13,8 +13,8 @@ mission to rescue the lost squad along the way and bring them home. This fix is enabled by default in the DFHack `control panel `, or you can run it as needed. However, it is still up to you to send out another squad that can be tasked with the rescue -mission. You can send the squad out on an innocuous "Demand tribute" mission to -minimize risk to the squad. +mission. You can send the squad out on a relatively innocuous mission, like +"Demand one-time tribute", to minimize risk to the squad. This tool is integrated with `gui/notify`, so you will get a notification in the DFHack notification panel when a squad is stuck and there are no squads diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 160f9f8136..76c835497f 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -323,12 +323,13 @@ NOTIFICATIONS_BY_IDX = { end, on_click=function() local message = 'A squad is lost on the world map and needs rescue!\n\n' .. - 'Please send a squad out on a mission that will return to the fort.\n' .. + 'Please send a squad out on a mission that will return to the fort (e.g.\n' .. + 'a Demand one-time tribute mission, but not a Conquer and occupy mission).\n' .. 'They will rescue the stuck squad on their way home.' if not repeat_util.isScheduled('control-panel/fix/stuck-squad') then message = message .. '\n\n' .. - 'Please enable fix/stuck-squad in the DFHack control panel to allow\n'.. - 'the rescue mission to happen.' + 'Please enable fix/stuck-squad in the DFHack control panel to enable\n'.. + 'missions to rescue stuck squads.' end dlg.showMessage('Rescue stuck squads', message, COLOR_WHITE) end, From 28452f23ae593c0357a0a027c1b801f984d7d37a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 25 Dec 2024 03:35:50 -0800 Subject: [PATCH 253/811] match active caste flag instead of curse flag so goblins and other naturally immortal races can benefit also use getReadableName for output messages --- changelog.txt | 1 + docs/immortal-cravings.rst | 6 +++--- immortal-cravings.lua | 28 +++++++++++++++++----------- 3 files changed, 21 insertions(+), 14 deletions(-) diff --git a/changelog.txt b/changelog.txt index 796d6c63b7..af409165e6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking ## Removed diff --git a/docs/immortal-cravings.rst b/docs/immortal-cravings.rst index dcb3cb13d5..9ccb90cf5d 100644 --- a/docs/immortal-cravings.rst +++ b/docs/immortal-cravings.rst @@ -7,9 +7,9 @@ immortal-cravings When enabled, this script watches your fort for units that have no physiological need to eat or drink but still have personality needs that can only be satisfied -by eating or drinking (e.g. necromancers). This enables those units to help -themselves to a drink or a meal when they crave one and are not otherwise -occupied. +by eating or drinking (e.g. necromancers or goblins). This enables those units +to help themselves to a drink or a meal when they crave one and are not +otherwise occupied. Usage ----- diff --git a/immortal-cravings.lua b/immortal-cravings.lua index cb42238119..5ec1519931 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -79,7 +79,7 @@ local function goDrink(unit) return end dfhack.job.addWorker(job, unit) - local name = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) + local name = dfhack.units.getReadableName(unit) print(dfhack.df2console('immortal-cravings: %s is getting a drink'):format(name)) end @@ -101,7 +101,7 @@ local function goEat(unit) return end dfhack.job.addWorker(job, unit) - local name = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) + local name = dfhack.units.getReadableName(unit) print(dfhack.df2console('immortal-cravings: %s is getting something to eat'):format(name)) end @@ -171,20 +171,26 @@ local function unit_loop() end end +local function is_active_caste_flag(unit, flag_name) + return not unit.curse.rem_tags1[flag_name] and + (unit.curse.add_tags1[flag_name] or dfhack.units.casteFlagSet(unit.race, unit.caste, df.caste_raw_flags[flag_name])) +end + ---main loop: look for citizens with personality needs for food/drink but w/o physiological need local function main_loop() -- print('immortal-cravings watching:') watched = {} for _, unit in ipairs(dfhack.units.getCitizens()) do - if unit.curse.add_tags1.NO_DRINK or unit.curse.add_tags1.NO_EAT then - for _, need in ipairs(unit.status.current_soul.personality.needs) do - if need.id == DrinkAlcohol and need.focus_level < threshold or - need.id == EatGoodMeal and need.focus_level < threshold - then - table.insert(watched, unit.id) - -- print(' '..dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit)))) - goto next_unit - end + if not is_active_caste_flag(unit, 'NO_DRINK') and not is_active_caste_flag(unit, 'NO_EAT') then + goto next_unit + end + for _, need in ipairs(unit.status.current_soul.personality.needs) do + if need.id == DrinkAlcohol and need.focus_level < threshold or + need.id == EatGoodMeal and need.focus_level < threshold + then + table.insert(watched, unit.id) + -- print(' '..dfhack.df2console(dfhack.units.getReadableName(unit))) + goto next_unit end end ::next_unit:: From 321eea30abfaf43773c9683cca153f102a66ccf9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 25 Dec 2024 03:59:35 -0800 Subject: [PATCH 254/811] add filter for written works --- changelog.txt | 1 + internal/caravan/pedestal.lua | 26 +++++++++++++++++++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index af409165e6..18bb88c425 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: ## Misc Improvements - `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking +- `caravan`: add filter for written works in display furniture assignment dialog ## Removed diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index d5ccfc3e1d..cd188914c1 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -25,6 +25,7 @@ local filters = { max_quality=6, hide_unreachable=true, hide_forbidden=false, + hide_written=false, inside_containers=true, } @@ -288,7 +289,7 @@ function AssignItems:init() }, widgets.ToggleHotkeyLabel{ view_id='hide_forbidden', - frame={t=2, l=40, w=30}, + frame={t=1, l=40, w=30}, label='Hide forbidden items:', key='CUSTOM_SHIFT_F', options={ @@ -302,6 +303,22 @@ function AssignItems:init() self:refresh_list() end, }, + widgets.ToggleHotkeyLabel{ + view_id='hide_written', + frame={t=3, l=40, w=30}, + label='Hide written items:', + key='CUSTOM_SHIFT_W', + options={ + {label='Yes', value=true, pen=COLOR_GREEN}, + {label='No', value=false} + }, + option_gap=5, + initial_option=filters.hide_written, + on_change=function(val) + filters.hide_written = val + self:refresh_list() + end, + }, }, }, widgets.Panel{ @@ -553,17 +570,24 @@ function AssignItems:cache_choices(inside_containers, display_bld) return choices end +local function is_written_work(item) + if df.item_bookst:is_instance(item) then return true end + return df.item_toolst:is_instance(item) and item:hasToolUse(df.tool_uses.CONTAIN_WRITING) +end + function AssignItems:get_choices() local raw_choices = self:cache_choices(self.subviews.inside_containers:getOptionValue(), self.bld) local choices = {} local include_unreachable = not self.subviews.hide_unreachable:getOptionValue() local include_forbidden = not self.subviews.hide_forbidden:getOptionValue() + local include_written = not self.subviews.hide_written:getOptionValue() local min_quality = self.subviews.min_quality:getOptionValue() local max_quality = self.subviews.max_quality:getOptionValue() for _,choice in ipairs(raw_choices) do local data = choice.data if not include_unreachable and not data.reachable then goto continue end if not include_forbidden and data.item.flags.forbid then goto continue end + if not include_written and is_written_work(data.item) then goto continue end if min_quality > data.quality then goto continue end if max_quality < data.quality then goto continue end table.insert(choices, choice) From 77e91ea8aa1db75374fcd000b1667c1873d3f33e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 25 Dec 2024 04:02:00 -0800 Subject: [PATCH 255/811] update docs --- docs/caravan.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/caravan.rst b/docs/caravan.rst index 3208514633..17cb2ed071 100644 --- a/docs/caravan.rst +++ b/docs/caravan.rst @@ -156,5 +156,9 @@ assignment GUI. The dialog allows you to sort by name, value, or where the item is currently assigned for display. -You can search by name, and you can filter by item quality and by whether the -item is forbidden. +You can search by name, and you can filter by: + +- item quality +- whether the item is forbidden +- whether the item is reachable from the display furniture +- whether the item is a written work (book or scroll) From a9e09e9eacbe91c4bc91a46f8d3fff07211aff65 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 25 Dec 2024 05:46:34 -0800 Subject: [PATCH 256/811] allow messengers to rescue stuck squads --- docs/fix/stuck-squad.rst | 20 +++++++------- fix/stuck-squad.lua | 44 ++++++++++++++++++++++++------- internal/notify/notifications.lua | 7 ++--- 3 files changed, 50 insertions(+), 21 deletions(-) diff --git a/docs/fix/stuck-squad.rst b/docs/fix/stuck-squad.rst index ec06913955..21a2d5048d 100644 --- a/docs/fix/stuck-squad.rst +++ b/docs/fix/stuck-squad.rst @@ -2,27 +2,29 @@ fix/stuck-squad =============== .. dfhack-tool:: - :summary: Allow squads returning from missions to rescue lost squads. + :summary: Allow squads and messengers to rescue lost squads. :tags: fort bugfix military Occasionally, squads that you send out on a mission get stuck on the world map. They lose their ability to navigate and are unable to return to your fortress. -This tool allows another of your squads that is (successfully) returning from a -mission to rescue the lost squad along the way and bring them home. +This tool allows a messenger that is returning from a holding or any other of +your squads that is returning from a mission to rescue the lost squad along the +way and bring them home. This fix is enabled by default in the DFHack `control panel `, or you can run it as needed. However, it -is still up to you to send out another squad that can be tasked with the rescue -mission. You can send the squad out on a relatively innocuous mission, like -"Demand one-time tribute", to minimize risk to the squad. +is still up to you to send out a messenger or squad that can be tasked with the +rescue. If you have a holding that is linked to your fort, you can send out a +messenger -- you don't have to actually request any workers. Otherwise, you can +send a squad out on a mission with minimal risk, like "Demand one-time tribute". This tool is integrated with `gui/notify`, so you will get a notification in -the DFHack notification panel when a squad is stuck and there are no squads -currently out traveling that can rescue them. +the DFHack notification panel when a squad is stuck and there are no squads or +messengers currently out traveling that can rescue them. Note that there might be other reasons why your squad appears missing -- if it got wiped out in combat and nobody survived to report back, for example -- but -this tool should fix the cases that are actual bugs. +this tool should allow you to recover from the cases that are actual bugs. Usage ----- diff --git a/fix/stuck-squad.lua b/fix/stuck-squad.lua index 987fabbdca..7bfc739b0d 100644 --- a/fix/stuck-squad.lua +++ b/fix/stuck-squad.lua @@ -16,10 +16,18 @@ end local function is_army_valid_and_returning(army) local controller = get_top_controller(army.controller) - if not controller or controller.goal ~= df.army_controller_goal_type.SITE_INVASION then - return false, false + if not controller then return false, false end + if controller.goal == df.army_controller_goal_type.SITE_INVASION then + return true, controller.data.goal_site_invasion.flag.RETURNING_HOME + elseif controller.goal == df.army_controller_goal_type.MAKE_REQUEST then + return true, controller.data.goal_make_request.flag.RETURNING_HOME end - return true, controller.data.goal_site_invasion.flag.RETURNING_HOME + return false, false +end + +local function get_hf_army(hf) + if not hf then return end + return df.army.find(hf.info and hf.info.whereabouts and hf.info.whereabouts.army_id or -1) end -- need to check all squad positions since some members may have died @@ -28,7 +36,7 @@ local function get_squad_army(squad) for _,sp in ipairs(squad.positions) do local hf = df.historical_figure.find(sp.occupant) if not hf then goto continue end - local army = df.army.find(hf.info and hf.info.whereabouts and hf.info.whereabouts.army_id or -1) + local army = get_hf_army(hf) if army then return army end ::continue:: end @@ -58,6 +66,24 @@ function scan_fort_armies() end ::continue:: end + + if #stuck_armies == 0 then return stuck_armies, nil, nil end + + -- prefer returning with a messenger if one is readily available + for _,messenger in ipairs(dfhack.units.getUnitsByNobleRole('Messenger')) do + local army = get_hf_army(df.historical_figure.find(messenger.hist_figure_id)) + if not army then goto continue end + local valid, returning = is_army_valid_and_returning(army) + if valid then + if returning then + returning_army = {army=army} + else + outbound_army = {army=army} + end + end + ::continue:: + end + return stuck_armies, outbound_army, returning_army end @@ -67,14 +93,14 @@ local function unstick_armies() if not returning_army then local instructions = outbound_army and ('Please wait for %s to complete their objective and run this command again when they are on their way home.'):format( - dfhack.df2console(dfhack.military.getSquadName(outbound_army.squad.id))) - or 'Please send a squad out on a mission that will return to the fort, and'.. + outbound_army.squad and dfhack.df2console(dfhack.military.getSquadName(outbound_army.squad.id)) or 'the messenger') + or 'Please send a squad or a messenger out on a mission that will return to the fort, and'.. ' run this command again when they are on the way home.' - qerror(('%d stuck arm%s found, but no returning armies found to rescue them!\n%s'):format( - #stuck_armies, #stuck_armies == 1 and 'y' or 'ies', instructions)) + qerror(('%d stuck squad%s found, but no returning squads or messengers are available to rescue them!\n%s'):format( + #stuck_armies, #stuck_armies == 1 and '' or 's', instructions)) return end - local returning_squad_name = dfhack.df2console(dfhack.military.getSquadName(returning_army.squad.id)) + local returning_squad_name = returning_army.squad and dfhack.df2console(dfhack.military.getSquadName(returning_army.squad.id)) or 'the messenger' for _,stuck in ipairs(stuck_armies) do print(('fix/stuck-squad: Squad rescue operation underway! %s is rescuing %s'):format( returning_squad_name, dfhack.military.getSquadName(stuck.squad.id))) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 76c835497f..0d312769d2 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -323,9 +323,10 @@ NOTIFICATIONS_BY_IDX = { end, on_click=function() local message = 'A squad is lost on the world map and needs rescue!\n\n' .. - 'Please send a squad out on a mission that will return to the fort (e.g.\n' .. - 'a Demand one-time tribute mission, but not a Conquer and occupy mission).\n' .. - 'They will rescue the stuck squad on their way home.' + 'Please send a messenger to a holding or a squad out on a mission\n' .. + 'that will return to the fort (e.g. a Demand one-time tribute mission,\n' .. + 'but not a Conquer and occupy mission). They will rescue the stuck\n' .. + 'squad on their way home.' if not repeat_util.isScheduled('control-panel/fix/stuck-squad') then message = message .. '\n\n' .. 'Please enable fix/stuck-squad in the DFHack control panel to enable\n'.. From 6cc1ec301245a760cb289ed422422e9cabf32a0e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 25 Dec 2024 21:48:50 -0800 Subject: [PATCH 257/811] changelog edit --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 18bb88c425..0894d4071f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,7 +27,7 @@ Template for new versions: # Future ## New Tools -- `fix/stuck-squad`: allow squads returning from missions to rescue other squads that have gotten stuck on the world map +- `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map ## New Features From de16c39f19840156ef103a3c30457fe5a11f1a4c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Dec 2024 00:57:09 -0800 Subject: [PATCH 258/811] add overlay for editing reserved_barrels setting --- changelog.txt | 2 + docs/gui/settings-manager.rst | 11 ++++-- gui/settings-manager.lua | 70 ++++++++++++++++++++++++++++++++++- 3 files changed, 78 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 0894d4071f..b3f8e5b920 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,8 @@ Template for new versions: - `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map ## New Features +- `gui/settings-manager`: new overlay on the Labor -> Standing Orders tab for configuring the number of barrels to reserve for job use (so you can brew alcohol and not have all your barrels claimed by stockpiles for container storage) +- `gui/settings-manager`: standing orders save/load now includes the reserved barrels setting ## Fixes diff --git a/docs/gui/settings-manager.rst b/docs/gui/settings-manager.rst index 1c57fe1a67..fd5bec8a51 100644 --- a/docs/gui/settings-manager.rst +++ b/docs/gui/settings-manager.rst @@ -39,10 +39,13 @@ back. You can also toggle an option to automatically load the saved settings for new embarks. When a fort is loaded, you can also go to the Labor -> Standing Orders page. -You will see a new panel that allows you to save and restore your settings for -standing orders. You can also toggle whether the saved standing orders are -automatically restored when you embark on a new fort. This will toggle the -relevant command in `gui/control-panel` on the Automation -> Autostart page. +You will see tow new panels. The first allows you to configure how many barrels +to reserve for use by workshop jobs (instead of being claimed by stockpiles for +container storage). The second panel allows you to save and restore your +settings for standing orders. You can also toggle whether the saved standing +orders are automatically restored when you embark on a new fort. This will +toggle the relevant command in `gui/control-panel` on the Automation -> +Autostart page. There is a similar panel on the Labor -> Work Details page that allows for saving and restoring of work detail definitions. Be aware that work detail diff --git a/gui/settings-manager.lua b/gui/settings-manager.lua index 1a7e468bf5..140310c2e4 100644 --- a/gui/settings-manager.lua +++ b/gui/settings-manager.lua @@ -2,6 +2,7 @@ local argparse = require('argparse') local control_panel = reqscript('control-panel') +local dialogs = require('gui.dialogs') local gui = require('gui') local json = require('json') local overlay = require('plugins.overlay') @@ -376,7 +377,9 @@ end -- StandingOrdersOverlay -- -local li = df.global.plotinfo.labor_info +local plotinfo = df.global.plotinfo +local li = plotinfo.labor_info +local ps = plotinfo.stockpile local function save_standing_orders() local standing_orders = {} @@ -390,6 +393,7 @@ local function save_standing_orders() chores.enabled = li.flags.children_do_chores chores.labors = utils.clone(li.chores) config.data.chores = chores + config.data.stockpile = {reserved_barrels=ps.reserved_barrels} config:write() end @@ -401,6 +405,10 @@ local function load_standing_orders() for i, val in ipairs(safe_index(config.data.chores, 'labors') or {}) do li.chores[i-1] = val end + local reserved_barrels = safe_index(config.data.stockpile, 'reserved_barrels') + if reserved_barrels then + ps.reserved_barrels = reserved_barrels + end end local function has_saved_standing_orders() @@ -422,6 +430,65 @@ StandingOrdersOverlay.ATTRS { autostart_command='gui/settings-manager load-standing-orders', } +------------------------------ +-- ReservedBarrelsOverlay +-- + +ReservedBarrelsOverlay = defclass(ReservedBarrelsOverlay, overlay.OverlayWidget) +ReservedBarrelsOverlay.ATTRS { + desc='Exposes the setting for reserved barrels on the standing orders screen.', + default_pos={x=59, y=18}, + default_enabled=true, + viewscreens='dwarfmode/Info/LABOR/STANDING_ORDERS/AUTOMATED_WORKSHOPS', + frame={w=26, h=6}, + frame_style=gui.MEDIUM_FRAME, + frame_background=gui.CLEAR_PEN, +} + +function ReservedBarrelsOverlay:init() + self:addviews{ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Barrels reserved for use', NEWLINE, + 'by workshop jobs: ', + { + text=function() return ps.reserved_barrels end, + pen=function() return ps.reserved_barrels == 0 and COLOR_YELLOW or COLOR_GREEN end, + }, + }, + }, + widgets.HotkeyLabel{ + frame={t=3, l=0}, + key='CUSTOM_CTRL_B', + label='Set num reserved', + on_activate=function() + dialogs.InputBox{ + frame_title='Set reserved barrels', + text={ + 'You can ensure a number of barrels are reserved, preventing', NEWLINE, + 'them from being claimed by stockpiles as container storage.', NEWLINE, + 'For example, if there are 5 reserved barrels, no stockpile', NEWLINE, + 'will claim an empty barrel for storing items until you have', NEWLINE, + 'at least 6 barrels lying around.', NEWLINE, NEWLINE, + 'This feature is most often used to ensure that a fortress has', NEWLINE, + 'ample empty barrels for the production of alcohol, although', NEWLINE, + 'empty barrels are also necessary for other jobs.', + }, + text_pen=COLOR_YELLOW, + label_text='Number of barrels to reserve: ', + input=tostring(ps.reserved_barrels), + on_input=function(input) + input = tonumber(input) + if not input or input < 0 then return end + ps.reserved_barrels = math.floor(input) + end, + }:show() + end, + }, + } +end + ------------------------------ -- WorkDetailsOverlay -- @@ -510,6 +577,7 @@ OVERLAY_WIDGETS = { embark_notification=DifficultyEmbarkNotificationOverlay, settings_difficulty=DifficultySettingsOverlay, standing_orders=StandingOrdersOverlay, + reserved_barrels=ReservedBarrelsOverlay, work_details=WorkDetailsOverlay, } From 4d888cfa8a30c42f6754dcb45f18f13c2270779c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 28 Dec 2024 09:06:37 -0800 Subject: [PATCH 259/811] don't empty buckets for in-use wells --- changelog.txt | 1 + fix/dry-buckets.lua | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/changelog.txt b/changelog.txt index b3f8e5b920..de714b749b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: - `gui/settings-manager`: standing orders save/load now includes the reserved barrels setting ## Fixes +- `fix/dry-buckets`: don't empty buckets for wells that are actively in use ## Misc Improvements - `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking diff --git a/fix/dry-buckets.lua b/fix/dry-buckets.lua index 71d2c50468..24a62a90f0 100644 --- a/fix/dry-buckets.lua +++ b/fix/dry-buckets.lua @@ -11,6 +11,11 @@ local emptied = 0 local in_building = 0 for _,item in ipairs(df.global.world.items.other.BUCKET) do if item.flags.in_job then goto continue end + local well = dfhack.items.getHolderBuilding(item) + if well and well:getType() == df.building_type.Well and well.well_tag.whole ~= 0 then + -- bucket is in a well and the well is actively being used + goto continue + end local emptied_bucket = false local freed_in_building = false for _,contained_item in ipairs(dfhack.items.getContainedItems(item)) do From 571e1c26479e8e6f27607b8465d13290dfab584d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 28 Dec 2024 21:12:25 -0800 Subject: [PATCH 260/811] don't poof wildlife that is currently onscreen so players aren't confused if they happen to have the viewscreen nearby --- changelog.txt | 1 + fix/wildlife.lua | 18 ++++++++++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index de714b749b..177604bc69 100644 --- a/changelog.txt +++ b/changelog.txt @@ -39,6 +39,7 @@ Template for new versions: ## Misc Improvements - `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking - `caravan`: add filter for written works in display furniture assignment dialog +- `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) ## Removed diff --git a/fix/wildlife.lua b/fix/wildlife.lua index 336cb701e8..c0d199259a 100644 --- a/fix/wildlife.lua +++ b/fix/wildlife.lua @@ -2,6 +2,7 @@ local argparse = require('argparse') local exterminate = reqscript('exterminate') +local guidm = require('gui.dwarfmode') local GLOBAL_KEY = 'fix/wildlife' @@ -61,7 +62,7 @@ local function refund_population(entry) end end --- refund unit to population and ensure it doesn't get picked up by unstick_surface_wildlife in the future +-- refund unit to population and ensure it doesn't get picked up by unstick_wildlife in the future local function detach_unit(unit) unit.flags2.roaming_wilderness_population_source = false unit.flags2.roaming_wilderness_population_source_not_a_map_feature = false @@ -74,6 +75,7 @@ local TICKS_PER_MONTH = 28 * TICKS_PER_DAY local TICKS_PER_SEASON = 3 * TICKS_PER_MONTH local TICKS_PER_YEAR = 4 * TICKS_PER_SEASON +-- time checks near year turnover is wishy-washy until we have a datetime API available local WEEK_BEFORE_EOY_TICKS = TICKS_PER_YEAR - TICKS_PER_WEEK -- update stuck_creatures records and check timeout @@ -113,9 +115,15 @@ function free_all_wildlife(include_hidden) end end -local function unstick_surface_wildlife(opts) +local function is_onscreen(unit, viewport) + viewport = viewport or guidm.Viewport.get() + return viewport:isVisible(xyz2pos(dfhack.units.getPosition(unit))), viewport +end + +local function unstick_wildlife(opts) local unstuck = {} local week_ago_ticks = math.max(0, df.global.cur_year_tick - TICKS_PER_WEEK) + local viewport for _,unit in ipairs(df.global.world.units.active) do if not is_active_wildlife(unit) or unit.animal.leave_countdown > 0 then goto skip @@ -132,7 +140,9 @@ local function unstick_surface_wildlife(opts) unstuck_entry.count = unstuck_entry.count + 1 if not opts.dry_run then stuck_creatures[unit.id] = nil - exterminate.killUnit(unit, exterminate.killMethod.DISINTEGRATE) + local unit_is_visible + unit_is_visible, viewport = is_onscreen(unit, viewport) + exterminate.killUnit(unit, not unit_is_visible and exterminate.killMethod.DISINTEGRATE or nil) end ::skip:: end @@ -188,5 +198,5 @@ if positionals[1] == 'ignore' then print(('%s will now be ignored by fix/wildlife'):format(dfhack.units.getReadableName(unit))) end else - unstick_surface_wildlife(opts) + unstick_wildlife(opts) end From 7ba6253069521abf5b2447f8d6fb1d8faf5d2bd7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 28 Dec 2024 21:19:44 -0800 Subject: [PATCH 261/811] refresh map sprite when profession is changed --- changelog.txt | 1 + internal/gm-unit/editor_profession.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 177604bc69..f4082534bd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -40,6 +40,7 @@ Template for new versions: - `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking - `caravan`: add filter for written works in display furniture assignment dialog - `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) +- `gui/gm-unit`: refresh unit sprite when profession is changed ## Removed diff --git a/internal/gm-unit/editor_profession.lua b/internal/gm-unit/editor_profession.lua index 0fb279fc55..fdf70a6a67 100644 --- a/internal/gm-unit/editor_profession.lua +++ b/internal/gm-unit/editor_profession.lua @@ -44,6 +44,7 @@ end function Editor_Prof:save_profession(_, choice) self.target_unit.profession = choice.profession self.target_unit.profession2 = choice.profession + self.target_unit.flags4.any_texture_must_be_refreshed = true end function Editor_Prof:onOpen() From 6545b28cdc5145d8793fc0d146aabcede5dcdc47 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 29 Dec 2024 00:41:03 -0800 Subject: [PATCH 262/811] use xp formula instead of limited range of enum values --- changelog.txt | 1 + internal/unit-info-viewer/skills-progress.lua | 11 +++++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index f4082534bd..6e40095559 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: ## Fixes - `fix/dry-buckets`: don't empty buckets for wells that are actively in use +- `gui/unit-info-viewer`: skill progress bars now show correct XP thresholds for skills past Legendary+5 ## Misc Improvements - `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking diff --git a/internal/unit-info-viewer/skills-progress.lua b/internal/unit-info-viewer/skills-progress.lua index 7e7dd17149..fdf9cb5bdc 100644 --- a/internal/unit-info-viewer/skills-progress.lua +++ b/internal/unit-info-viewer/skills-progress.lua @@ -27,7 +27,6 @@ SkillProgressOverlay.ATTRS { 'dwarfmode/ViewSheets/UNIT/Skills/Combat', 'dwarfmode/ViewSheets/UNIT/Skills/Social', 'dwarfmode/ViewSheets/UNIT/Skills/Other', - 'dungeonmode/ViewSheets/UNIT/Skills/Labor', 'dungeonmode/ViewSheets/UNIT/Skills/Combat', 'dungeonmode/ViewSheets/UNIT/Skills/Social', @@ -79,6 +78,10 @@ function SkillProgressOverlay:preUpdateLayout(parent_rect) self.frame.h = parent_rect.height - 21 end +local function get_threshold(lvl) + return 500 + lvl * 100 +end + function SkillProgressOverlay:onRenderFrame(dc, rect) local annotations = {} local current_unit = df.unit.find(view_sheets.active_id) @@ -104,7 +107,7 @@ function SkillProgressOverlay:onRenderFrame(dc, rect) table.insert(annotations, "\n\n\n\n") goto continue end - local rating = df.skill_rating.attrs[math.max(df.skill_rating.Dabbling, math.min(skill.rating, df.skill_rating.Legendary5))] + local xp_threshold = get_threshold(skill.rating) if experience then if not progress_bar then table.insert(annotations, NEWLINE) @@ -122,7 +125,7 @@ function SkillProgressOverlay:onRenderFrame(dc, rect) pen=level_color, }) table.insert(annotations, { - text=('%4d/%4d'):format(skill.experience, rating.xp_threshold), + text=('%4d/%4d'):format(skill.experience, xp_threshold), pen=level_color, width=9, rjustify=true, @@ -134,7 +137,7 @@ function SkillProgressOverlay:onRenderFrame(dc, rect) -- Progress Bar if progress_bar then table.insert(annotations, NEWLINE) - local percentage = skill.experience / rating.xp_threshold + local percentage = skill.experience / xp_threshold local barstop = math.floor((margin * percentage) + 0.5) for i = 0, margin-1 do local color = COLOR_LIGHTCYAN From 32625912aae2112eaaea2b5456ffd39b11e1f697 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 29 Dec 2024 16:29:35 -0800 Subject: [PATCH 263/811] fix whitespace --- docs/gui/unit-info-viewer.rst | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/gui/unit-info-viewer.rst b/docs/gui/unit-info-viewer.rst index 6139bb266c..ed9d29a2b2 100644 --- a/docs/gui/unit-info-viewer.rst +++ b/docs/gui/unit-info-viewer.rst @@ -5,8 +5,8 @@ gui/unit-info-viewer :summary: Display detailed information about a unit. :tags: adventure fort interface inspection units -When run, it displays information about age, birth, maxage, shearing, milking, grazing, egg -laying, body size, and death for the selected unit. +When run, it displays information about age, birth, maxage, shearing, milking, +grazing, egg laying, body size, and death for the selected unit. You can click on different units while the tool window is open and the displayed information will refresh for the selected unit. @@ -21,8 +21,8 @@ Usage Overlays -------- -This tool adds progress bars, experience points and levels in the unit skill panels, -color-coded to highlight rust and the highest skill levels: +This tool adds progress bars, experience points and levels in the unit skill +panels, color-coded to highlight rust and the highest skill levels: - If a skill is rusty, then the level marker is colored light red - If a skill is at Legendary level or higher, it is colored light cyan From 0f7d46c649f1b3f4f2553968812bad07ca405eb1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 30 Dec 2024 17:45:58 -0800 Subject: [PATCH 264/811] also set prof of hf when changing profession --- internal/gm-unit/editor_profession.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/gm-unit/editor_profession.lua b/internal/gm-unit/editor_profession.lua index fdf70a6a67..79f6ac928a 100644 --- a/internal/gm-unit/editor_profession.lua +++ b/internal/gm-unit/editor_profession.lua @@ -44,6 +44,10 @@ end function Editor_Prof:save_profession(_, choice) self.target_unit.profession = choice.profession self.target_unit.profession2 = choice.profession + local hf = df.historical_figure.find(self.target_unit.hist_figure_id) + if hf then + hf.profession = choice.profession + end self.target_unit.flags4.any_texture_must_be_refreshed = true end From 4a7afa29530064ed98fe45818a243d79da9384d7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 31 Dec 2024 18:42:49 -0800 Subject: [PATCH 265/811] consolify item command output --- item.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/item.lua b/item.lua index a6dc37712c..7b91bdbf8a 100644 --- a/item.lua +++ b/item.lua @@ -293,7 +293,7 @@ function execute(action, conditions, options, return_items) end table.sort(desc_list) for _, desc in ipairs(desc_list) do - print(('%4d %s'):format(descriptions[desc], desc)) + print(('%4d %s'):format(descriptions[desc], dfhack.df2console(desc))) end return count, items, types From b95028c5beb5380d89365c21f4b33ce248e54376 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 1 Jan 2025 05:48:01 -0800 Subject: [PATCH 266/811] show visitor and invader affiliations --- changelog.txt | 1 + gui/sitemap.lua | 29 ++++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 6e40095559..5b8aa1c065 100644 --- a/changelog.txt +++ b/changelog.txt @@ -42,6 +42,7 @@ Template for new versions: - `caravan`: add filter for written works in display furniture assignment dialog - `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) - `gui/gm-unit`: refresh unit sprite when profession is changed +- `gui/sitemap`: show primary group affiliation for visitors and invaders (e.g. civilization name or performance troupe) ## Removed diff --git a/gui/sitemap.lua b/gui/sitemap.lua index 5dd34b48e3..4440cdd730 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -9,8 +9,9 @@ local widgets = require('gui.widgets') Sitemap = defclass(Sitemap, widgets.Window) Sitemap.ATTRS { frame_title='Sitemap', - frame={w=47, r=2, t=18, h=23}, + frame={w=57, r=2, t=18, h=25}, resizable=true, + resize_min={w=43, h=20}, } local function to_title_case(str) @@ -99,23 +100,41 @@ local function zoom_to_next_zone(_, choice) data.next_idx = data.next_idx % #data.zones + 1 end -local function get_unit_disposition_and_pen(unit) +local function get_affiliation(unit) + local he = df.historical_entity.find(unit.civ_id) + if not he then return 'Unknown affiliation' end + local et_name = dfhack.TranslateName(he.name, true) + local et_type = df.historical_entity_type[he.type]:gsub('(%l)(%u)', '%1 %2') + return ('%s%s %s'):format(#et_name > 0 and et_name or 'Unknown', #et_name > 0 and ',' or '', et_type) +end + +local function get_unit_disposition_and_pen_and_affiliation(unit) local prefix = unit.flags1.caged and 'caged ' or '' if dfhack.units.isDanger(unit) then + if dfhack.units.isInvader(unit) then + return prefix..'invader', COLOR_RED, get_affiliation(unit) + end return prefix..'hostile', COLOR_LIGHTRED - end - if not dfhack.units.isFortControlled(unit) and dfhack.units.isWildlife(unit) then + elseif dfhack.units.isFortControlled(unit) then + return prefix..'fort '..(dfhack.units.isAnimal(unit) and 'animal' or 'member'), COLOR_LIGHTBLUE + elseif dfhack.units.isWildlife(unit) then return prefix..'wildlife', COLOR_GREEN + elseif dfhack.units.isVisitor(unit) or dfhack.units.isDiplomat(unit) then + return prefix..'visitor', COLOR_MAGENTA, get_affiliation(unit) + elseif dfhack.units.isMerchant(unit) or dfhack.units.isForest(unit) then + return prefix..'merchant'..(dfhack.units.isAnimal(unit) and ' animal' or ''), COLOR_BROWN, get_affiliation(unit) end return prefix..'friendly', COLOR_LIGHTGREEN end local function get_unit_choice_text(unit) - local disposition, disposition_pen = get_unit_disposition_and_pen(unit) + local disposition, disposition_pen, affiliation = get_unit_disposition_and_pen_and_affiliation(unit) return { dfhack.units.getReadableName(unit), ' (', {text=disposition, pen=disposition_pen}, + affiliation and ': ' or '', + {text=affiliation, pen=COLOR_YELLOW}, ')', } end From 1b79147cce59e0288424ed2f5e438823e332185d Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 1 Jan 2025 11:34:58 -0600 Subject: [PATCH 267/811] Create helloSlider.lua Created helloSlider.lua (a prototype for a new single-slider widget) --- devel/helloSlider.lua | 212 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 212 insertions(+) create mode 100644 devel/helloSlider.lua diff --git a/devel/helloSlider.lua b/devel/helloSlider.lua new file mode 100644 index 0000000000..ff850d0982 --- /dev/null +++ b/devel/helloSlider.lua @@ -0,0 +1,212 @@ +local Widget = require('gui.widgets.widget') + +local to_pen = dfhack.pen.parse + +-------------------------------- +-- Slider +-------------------------------- + +---@class widgets.Slider.attrs: widgets.Widget.attrs +---@field num_stops integer +---@field get_idx_fn? function +---@field on_change? fun(index: integer) + +---@class widgets.Slider.attrs.partial: widgets.Slider.attrs + +---@class widgets.Slider.initTable: widgets.Slider.attrs +---@field num_stops integer + +---@class widgets.Slider: widgets.Widget, widgets.Slider.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) +---@overload fun(init_table: widgets.Slider.initTable): self +Slider = defclass(Slider, Widget) +Slider.ATTRS{ + num_stops=DEFAULT_NIL, + get_idx_fn=DEFAULT_NIL, + on_change=DEFAULT_NIL, +} + +function Slider:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function Slider:init() + if self.num_stops < 2 then error('too few Slider stops') end + self.is_dragging_target = nil -- 'left', 'right', or 'both' + self.is_dragging_idx = nil -- offset from leftmost dragged tile +end + +local function Slider_get_width_per_idx(self) + return math.max(3, (self.frame_body.width-7) // (self.num_stops-1)) +end + +function Slider:onInput(keys) + if not keys._MOUSE_L then return false end + local x = self:getMousePos() + if not x then return false end + local left_idx = self.get_idx_fn() + local width_per_idx = Slider_get_width_per_idx(self) + local left_pos = width_per_idx*(left_idx-1) + local right_pos = width_per_idx*(left_idx-1) + 4 + if x < left_pos then + self.on_change(self.get_idx_fn() - 1) + else + self.is_dragging_target = 'both' + self.is_dragging_idx = x - right_pos + end + return true +end + +local function Slider_do_drag(self, width_per_idx) + local x = self.frame_body:localXY(dfhack.screen.getMousePos()) + local cur_pos = x - self.is_dragging_idx + cur_pos = math.max(0, cur_pos) + cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) + local offset = 1 + local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 + if self.is_dragging_target == 'both' then + if new_idx > self.num_stops then + return + end + end + if new_idx and new_idx ~= self.get_idx_fn() then + self.on_change(new_idx) + end +end + +local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} + +function Slider:onRenderBody(dc, rect) + local left_idx = self.get_idx_fn() + local width_per_idx = Slider_get_width_per_idx(self) + -- draw track + dc:seek(1,0) + dc:char(nil, SLIDER_LEFT_END) + dc:char(nil, SLIDER_TRACK) + for stop_idx=1,self.num_stops-1 do + local track_stop_pen = SLIDER_TRACK_STOP_SELECTED + local track_pen = SLIDER_TRACK_SELECTED + if left_idx ~= stop_idx then + track_stop_pen = SLIDER_TRACK_STOP + track_pen = SLIDER_TRACK + elseif left_idx == stop_idx then + track_pen = SLIDER_TRACK + end + dc:char(nil, track_stop_pen) + for i=2,width_per_idx do + dc:char(nil, track_pen) + end + end + if left_idx >= self.num_stops then + dc:char(nil, SLIDER_TRACK_STOP_SELECTED) + else + dc:char(nil, SLIDER_TRACK_STOP) + end + dc:char(nil, SLIDER_TRACK) + dc:char(nil, SLIDER_RIGHT_END) + -- draw tab + dc:seek(width_per_idx*(left_idx-1)+2) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + -- manage dragging + if self.is_dragging_target then + Slider_do_drag(self, width_per_idx) + end + if df.global.enabler.mouse_lbut_down == 0 then + self.is_dragging_target = nil + self.is_dragging_idx = nil + end +end + + + + + + + + + + + +local gui = require('gui') +local widgets = require('gui.widgets') + +-- +-- RangerWindow +-- + +RangerWindow = defclass(RangerWindow, widgets.Window) +RangerWindow.ATTRS { + frame_title='Hello, Slider!', + frame={w=25, h=8}, + resizable=true, + resize_min={w=25, h=8}, +} + +function RangerWindow:init() + local LEVEL_OPTIONS = { + {label='Low', value=1}, + {label='Medium', value=2}, + {label='High', value=3}, + {label='Pro', value=4}, + {label='Insane', value=5}, + } + + self:addviews{ + widgets.CycleHotkeyLabel{ + view_id='level', + frame={l=1, t=0, w=16}, + label='Level:', + label_below=true, + key_back='CUSTOM_SHIFT_C', + key='CUSTOM_SHIFT_V', + options=LEVEL_OPTIONS, + initial_option=LEVEL_OPTIONS[1].value, + on_change=function(val) + self.subviews.level:setOption(val) + end, + }, + Slider{ + frame={l=1, t=3}, + num_stops=#LEVEL_OPTIONS, + get_idx_fn=function() + return self.subviews.level:getOptionValue() + end, + on_change=function(idx) self.subviews.level:setOption(idx) end, + }, + } +end + +-- +-- RangerScreen +-- + +RangerScreen = defclass(RangerScreen, gui.ZScreen) +RangerScreen.ATTRS { + focus_path='ranger', +} + +function RangerScreen:init() + self:addviews{RangerWindow{}} +end + +function RangerScreen:onDismiss() + view = nil +end + +-- +-- main logic +-- + +view = view and view:raise() or RangerScreen{}:show() \ No newline at end of file From 9d68acaef29eac40be4cbd85a7541d41a2e423e0 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 1 Jan 2025 11:37:53 -0600 Subject: [PATCH 268/811] Update helloSlider.lua to try and fix precommit EOF errors --- devel/helloSlider.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/helloSlider.lua b/devel/helloSlider.lua index ff850d0982..f22b7c7a6b 100644 --- a/devel/helloSlider.lua +++ b/devel/helloSlider.lua @@ -209,4 +209,4 @@ end -- main logic -- -view = view and view:raise() or RangerScreen{}:show() \ No newline at end of file +view = view and view:raise() or RangerScreen{}:show() From a67e1e30c51ed1c577cb7f247b7bbca384744537 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 29 Dec 2024 16:29:57 -0800 Subject: [PATCH 269/811] prep for initial version of gui/rename --- docs/gui/rename.rst | 78 +++++++++++--- gui/rename.lua | 258 +++++++++++++++++++++++++------------------- 2 files changed, 212 insertions(+), 124 deletions(-) diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index 5688354bb7..d72255d0ac 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -2,28 +2,74 @@ gui/rename ========== .. dfhack-tool:: - :summary: Give buildings and units new names, optionally with special chars. - :tags: unavailable + :summary: Modify the name of anything that is nameable. + :tags: adventure fort productivity animals items units -Once you select a target on the game map, this tool allows you to rename it. It -is more powerful than the in-game rename functionality since it allows you to -use special characters (like diamond symbols), and it also allows you to rename -enemies and overwrite animal species strings. - -This tool supports renaming units, zones, stockpiles, workshops, furnaces, -traps, and siege engines. +Once you select a target (by clicking on the game map, by passing a commandline +parameter, or by using the provided selection widget) this tool allows you +change its language name, generate a new random name, or rename it with your +preferred component words. It provides an interface similar to the in-game +naming panel that you can use to customize your fortress name at embark. That +is, it allows you to choose words from an in-game language to assemble a name, +just like the default names that the game generates. You will be able to assign +units new given and last names. You can also use this tool to set freeform +"nicknames" for targets that support it. Usage ----- +:: + + gui/rename [] + +Examples +-------- + ``gui/rename`` - Renames the selected building, zone, or unit. -``gui/rename unit-profession`` - Set the unit profession or the animal species string. + Load the selected artifact, location, or unit for renaming. If nothing is + selected, you can select a target from a list. +``gui/rename -u 123 --no-target-selector`` + Load the unit with id ``123`` for renaming and remove the widget that + allows selecting a different target. +``gui/rename --location 2 --site 456`` + Load the location with "abstract building" ID ``2`` attached to the site + with id ``456`` for renaming. + +Options +------- + +``-a``, ``--artifact `` + Rename the artifact with the given item ID. +``-e``, ``--entity `` + Rename the historical entity (e.g. site government, world religion, etc) + with the given ID. +``-f``, ``--histfig `` + Rename the historical figure with the given ID. +``-l``, ``--location `` + Rename the location (e.g. tavern, hospital, guildhall, temple) with the + given ID. If this option is used, ``--site`` can be specified to indicate + locations attached to a specific site. If ``--site`` is not specified, the + location will be loaded from the current site. +``-q``, ``--squad `` + Rename the squad with the given ID. +``-s``, ``--site `` + Rename the site with the given ID. +``-u``, ``--unit `` + Rename the unit with the given ID. Renaming a unit also renames the + associated historical figure. +``-w``, ``--world`` + Rename the current world. +``--no-target-selector`` + Do not allow the player to switch naming targets. An option that sets the + initial target is required when using this option. -Screenshots ------------ +Overlays +-------- -.. image:: /docs/images/rename-bld.png +This tool supports the following overlays: -.. image:: /docs/images/rename-prof.png +``gui/rename.embark`` + Adds widgets to the embark preparation screen for renaming the starting + dwarves. +``gui/rename.world`` + Adds a widget to the world generation screen for renaming the world. diff --git a/gui/rename.lua b/gui/rename.lua index 509b61ba0d..1a05cbe87b 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -1,127 +1,169 @@ --- Rename various objects via gui. ---[====[ +local argparse = require('argparse') +local gui = require('gui') +local utils = require('utils') +local widgets = require('gui.widgets') + +-- +-- Rename +-- + +Rename = defclass(Rename, widgets.Window) +Rename.ATTRS { + frame_title='Rename', + frame={w=87, h=30}, + resizable=true, + resize_mid={w=50, h=20}, +} + +function Rename:init(info) + self.target = info.target + self.sync_targets = info.sync_targets or {} -gui/rename -========== -Backed by `rename`, this script allows entering the desired name -via a simple dialog in the game ui. - -* ``gui/rename [building]`` in :kbd:`q` mode changes the name of a building. - - .. image:: /docs/images/rename-bld.png - - The selected building must be one of stockpile, workshop, furnace, trap, or siege engine. - It is also possible to rename zones from the :kbd:`i` menu. - -* ``gui/rename [unit]`` with a unit selected changes the nickname. - - Unlike the built-in interface, this works even on enemies and animals. + self:addviews{ + widgets.Label{ + text={ + self.target and dfhack.TranslateName(self.target) or 'No target', NEWLINE, + self.target and dfhack.TranslateName(self.target, true), + }, + auto_width=true, + }, + } +end -* ``gui/rename unit-profession`` changes the selected unit's custom profession name. +-- +-- RenameScreen +-- - .. image:: /docs/images/rename-prof.png +RenameScreen = defclass(RenameScreen, gui.ZScreen) +RenameScreen.ATTRS { + focus_path='rename', +} - Likewise, this can be applied to any unit, and when used on animals it overrides - their species string. +function RenameScreen:init(info) + self:addviews{ + Rename{ + target=info.target, + sync_targets=info.sync_targets, + show_selector=info.show_selector, + } + } +end -The ``building`` or ``unit`` options are automatically assumed when in relevant UI state. +function RenameScreen:onDismiss() + view = nil +end -]====] -local gui = require 'gui' -local dlg = require 'gui.dialogs' -local widgets = require 'gui.widgets' -local plugin = require 'plugins.rename' +-- +-- CLI +-- -local mode = ... -local focus = dfhack.gui.getCurFocus() +if not dfhack.isWorldLoaded() then + qerror('This script requires a world to be loaded') +end -RenameDialog = defclass(RenameDialog, dlg.InputBox) -function RenameDialog:init(info) - self:addviews{ - widgets.Label{ - view_id = 'controls', - text = { - {key = 'CUSTOM_ALT_C', text = ': Clear, ', - on_activate = function() - self.subviews.edit.text = '' - end}, - {key = 'CUSTOM_ALT_S', text = ': Special chars', - on_activate = curry(dfhack.run_script, 'gui/cp437-table')}, - }, - frame = {b = 0, l = 0, r = 0, w = 70}, - } - } - -- calculate text_width once - self.subviews.controls:getTextWidth() +local function get_artifact_target(item) + if not item or not item.flags.artifact then return end + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) + if not gref then return end + local rec = df.artifact_record.find(gref.artifact_id) + if not rec then return end + return rec.name end -function RenameDialog:getWantedFrameSize() - local x, y = self.super.getWantedFrameSize(self) - x = math.max(x, self.subviews.controls.text_width) - return x, y + 2 +local function get_unit_target(unit, sync_targets) + if not unit then return end + local hf = df.historical_figure.find(unit.hist_figure_id) + if hf then table.insert(sync_targets, hf.name) end + return unit.name end -function showRenameDialog(title, text, input, on_input) - RenameDialog{ - frame_title = title, - text = text, - text_pen = COLOR_GREEN, - input = input, - on_input = on_input, - }:show() +local function get_location_target(site, loc_id) + if not site or loc_id < 0 then return end + local loc = utils.binsearch(site.buildings, loc_id, 'id') + if not loc then return end + return loc.name end -local function verify_mode(expected) - if mode ~= nil and mode ~= expected then - qerror('Invalid UI state for mode '..mode) +local function get_target(opts) + local target, sync_targets = nil, {} + if opts.histfig_id then + local hf = df.historical_figure.find(opts.histfig_id) + if not hf then qerror('Historical figure not found') end + target = hf.name + local unit = df.unit.find(hf.unit_id) + if unit then table.insert(sync_targets, unit.name) end + elseif opts.item_id then + target = get_artifact_target(df.item.find(opts.item_id)) + if not target then qerror('Artifact not found') end + elseif opts.location_id then + local site = opts.site_id and df.world_site.find(opts.site_id) or dfhack.world.getCurrentSite() + if not site then qerror('Site not found') end + target = get_location_target(site, opts.location_id) + if not target then qerror('Location not found') end + elseif opts.site_id then + local site = df.world_site.find(opts.site_id) + if not site then qerror('Site not found') end + target = site.name + elseif opts.squad_id then + local squad = df.squad.find(opts.squad_id) + if not squad then qerror('Squad not found') end + target = squad.name + elseif opts.unit_id then + target = get_unit_target(df.unit.find(opts.unit_id), sync_targets) + if not target then qerror('Unit not found') end + elseif opts.world then + target = df.global.world.world_data.name end + return target, sync_targets end -local unit = dfhack.gui.getSelectedUnit(true) -local building = dfhack.gui.getSelectedBuilding(true) - -if building and (not unit or mode == 'building') then - verify_mode('building') - - if plugin.canRenameBuilding(building) then - showRenameDialog( - 'Rename Building', - 'Enter a new name for the building:', - building.name, - curry(plugin.renameBuilding, building) - ) - else - dlg.showMessage( - 'Rename Building', - 'Cannot rename this type of building.', COLOR_LIGHTRED - ) - end -elseif unit then - if mode == 'unit-profession' then - showRenameDialog( - 'Rename Unit', - 'Enter a new profession for the unit:', - unit.custom_profession, - function(newval) - unit.custom_profession = newval - end - ) - else - verify_mode('unit') - - local vname = dfhack.units.getVisibleName(unit) - local vnick = '' - if vname and vname.has_name then - vnick = vname.nickname - end - - showRenameDialog( - 'Rename Unit', - 'Enter a new nickname for the unit:', - vnick, - curry(dfhack.units.setNickname, unit) - ) +local opts = { + help=false, + entity_id=nil, + histfig_id=nil, + item_id=nil, + location_id=nil, + site_id=nil, + squad_id=nil, + unit_id=nil, + world=false, + show_selector=true, +} +local positionals = argparse.processArgsGetopt({...}, { + { 'a', 'artifact', handler=function(optarg) opts.item_id = argparse.nonnegativeInt(optarg, 'artifact') end }, + { 'e', 'entity', handler=function(optarg) opts.entity_id = argparse.nonnegativeInt(optarg, 'entity') end }, + { 'f', 'histfig', handler=function(optarg) opts.histfig_id = argparse.nonnegativeInt(optarg, 'histfig') end }, + { 'h', 'help', handler = function() opts.help = true end }, + { 'l', 'location', handler=function(optarg) opts.location_id = argparse.nonnegativeInt(optarg, 'location') end }, + { 'q', 'squad', handler=function(optarg) opts.squad_id = argparse.nonnegativeInt(optarg, 'squad') end }, + { 's', 'site', handler=function(optarg) opts.site_id = argparse.nonnegativeInt(optarg, 'site') end }, + { 'u', 'unit', handler=function(optarg) opts.unit_id = argparse.nonnegativeInt(optarg, 'unit') end }, + { 'w', 'world', handler=function() opts.world = true end }, + { '', 'no-target-selector', handler=function() opts.show_selector = false end }, +}) + +if opts.help or positionals[1] == 'help' then + print(dfhack.script_help()) + return +end + +local target, sync_targets = get_target(opts) + +if not target then + local unit = dfhack.gui.getSelectedUnit(true) + local item = dfhack.gui.getSelectedItem(true) + local zone = dfhack.gui.getSelectedCivZone(true) + if unit then + target = get_unit_target(unit, sync_targets) + elseif item then + target = get_artifact_target(item) + elseif zone then + target = get_location_target(df.world_site.find(zone.site_id), zone.location_id) end -elseif mode then - verify_mode(nil) end + +view = view and view:raise() or RenameScreen{ + target=target, + sync_targets=sync_targets, + show_selector=opts.show_selector +}:show() From 0654d7ebd0d32687662906b22592033afe195d28 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 31 Dec 2024 18:42:28 -0800 Subject: [PATCH 270/811] in progress --- gui/rename.lua | 274 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 261 insertions(+), 13 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 1a05cbe87b..2d57cfa999 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -3,6 +3,18 @@ local gui = require('gui') local utils = require('utils') local widgets = require('gui.widgets') +local CH_UP = string.char(30) +local CH_DN = string.char(31) +local ENGLISH_COL_WIDTH = 16 +local NATIVE_COL_WIDTH = 16 + +-- +-- target selection +-- + +local function select_new_target() +end + -- -- Rename -- @@ -12,22 +24,242 @@ Rename.ATTRS { frame_title='Rename', frame={w=87, h=30}, resizable=true, - resize_mid={w=50, h=20}, + resize_min={w=77, h=30}, } +local function get_language_options() + local options, max_width = {}, 5 + for idx, lang in ipairs(df.language_translation.get_vector()) do + max_width = math.max(max_width, #lang.name) + table.insert(options, {label=dfhack.capitalizeStringWords(dfhack.lowerCp437(lang.name)), value=idx, pen=COLOR_CYAN}) + end + return options, max_width +end + +local function pad_text(text, width) + return (' '):rep((width - #text)//2) .. text +end + +local function sort_by_english_desc(a, b) +end + +local function sort_by_english_asc(a, b) +end + +local function sort_by_native_desc(a, b) +end + +local function sort_by_native_asc(a, b) +end + +local function sort_by_part_of_speech_desc(a, b) +end + +local function sort_by_part_of_speech_asc(a, b) +end + function Rename:init(info) self.target = info.target self.sync_targets = info.sync_targets or {} + self.cache = {} + + local language_options, max_lang_name_width = get_language_options() self:addviews{ - widgets.Label{ - text={ - self.target and dfhack.TranslateName(self.target) or 'No target', NEWLINE, - self.target and dfhack.TranslateName(self.target, true), + widgets.Panel{frame={t=0, h=7}, -- header + subviews={ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + key='CUSTOM_CTRL_N', + label='Select new target', + on_activate=function() + local target, sync_targets = select_new_target() + if target then + self.target, self.sync_targets = target, sync_targets + self.subviews.language:setOption(self.target.language) + end + end, + visible=info.show_selector, + }, + widgets.HotkeyLabel{ + frame={t=0, r=0}, + label='Generate random name', + key='CUSTOM_CTRL_G', + on_activate=function() end, + auto_width=true, + }, + widgets.Label{ + frame={t=2}, + text={{pen=COLOR_YELLOW, text=function() return pad_text(dfhack.TranslateName(self.target), self.frame_body.width) end}}, + }, + widgets.Label{ + frame={t=3}, + text={{pen=COLOR_LIGHTCYAN, text=function() return pad_text(('"%s"'):format(dfhack.TranslateName(self.target, true)), self.frame_body.width) end}}, + }, + widgets.CycleHotkeyLabel{ + view_id='language', + frame={t=5, l=0, w=max_lang_name_width + 18}, + key='CUSTOM_CTRL_T', + label='Language:', + options=language_options, + initial_option=self.target and self.target.language or 0, + on_change=function(val) + self.target.language = val + for _, sync_target in ipairs(self.sync_targets) do + sync_target.language = val + end + end, + }, + widgets.Label{ + frame={t=6, l=7}, + text={'Name type: ', {pen=COLOR_CYAN, text=function() return df.language_name_type[self.target.type] end}}, + }, + }, + }, + widgets.Panel{frame={t=8}, -- body + subviews={ + widgets.Panel{frame={t=0, h=1}, -- toolbar + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={t=0, l=0, w=32}, + label='Sort by:', + key='CUSTOM_CTRL_O', + options={ + {label='English'..CH_DN, value=sort_by_english_desc}, + {label='English'..CH_UP, value=sort_by_english_asc}, + {label='native'..CH_DN, value=sort_by_native_desc}, + {label='native'..CH_UP, value=sort_by_native_asc}, + {label='part of speech'..CH_DN, value=sort_by_part_of_speech_desc}, + {label='part of speech'..CH_UP, value=sort_by_part_of_speech_asc}, + }, + initial_option=sort_by_english_desc, + on_change=self:callback('refresh_list', 'sort'), + }, + widgets.EditField{ + view_id='search', + frame={t=0, l=35}, + label_text='Search: ', + ignore_keys={'SECONDSCROLL_DOWN', 'SECONDSCROLL_UP'} + }, + }, + }, + widgets.Panel{frame={t=2, l=0, w=30}, -- component selector + subviews={ + widgets.List{frame={t=0, l=0, b=2}, + view_id='component_list', + on_select=function(idx, choice) print('component choice', idx) printall_recurse(choice) end, + choices=self:get_component_choices(), + row_height=2, + scroll_keys={ + SECONDSCROLL_UP = -1, + SECONDSCROLL_DOWN = 1, + }, + }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='SECONDSCROLL_UP', + label='Prev component', + on_activate=function() self.subviews.component_list:moveCursor(-1) end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + key='SECONDSCROLL_DOWN', + label='Next component', + on_activate=function() self.subviews.component_list:moveCursor(1) end, + }, + }, + }, + widgets.Panel{frame={t=2, l=30}, -- words table + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort_english', + frame={t=0, l=0, w=8}, + options={ + {label='English', value=DEFAULT_NIL}, + {label='English'..CH_DN, value=sort_by_english_desc}, + {label='English'..CH_UP, value=sort_by_english_asc}, + }, + initial_option=sort_by_english_desc, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_english'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_native', + frame={t=0, l=ENGLISH_COL_WIDTH+2, w=7}, + options={ + {label='native', value=DEFAULT_NIL}, + {label='native'..CH_DN, value=sort_by_native_desc}, + {label='native'..CH_UP, value=sort_by_native_asc}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_native'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_part_of_speech', + frame={t=0, l=ENGLISH_COL_WIDTH+2+NATIVE_COL_WIDTH+2, w=15}, + options={ + {label='part of speech', value=DEFAULT_NIL}, + {label='part_of_speech'..CH_DN, value=sort_by_part_of_speech_desc}, + {label='part_of_speech'..CH_UP, value=sort_by_part_of_speech_asc}, + }, + option_gap=0, + on_change=self:callback('refresh_list', 'sort_part_of_speech'), + }, + widgets.FilteredList{ + view_id='list', + frame={t=2, l=0, b=0, r=0}, + on_submit=function() end, + }, + }, + }, }, - auto_width=true, }, } + + -- replace the FilteredList's built-in EditField with our own + self.subviews.list.list.frame.t = 0 + self.subviews.list.edit.visible = false + self.subviews.list.edit = self.subviews.search + self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') + + self:refresh_list() +end + +function Rename:get_component_choices() + local choices = {} + for val, comp in ipairs(df.language_name_component) do + local text = { + {text=comp:gsub('(%l)(%u)', '%1 %2')}, NEWLINE + {text=function() + local word = self.target.words[val] + if word < 0 then return end + return ('word: %s'):format(df.global.world.raws.language.words[word].forms.Noun) + end} + } + table.insert(choices, {text=text, data={val=val}}) + end + return choices +end + +function Rename:get_word_choices() + --if self.cache[] + local translations = df.language_translation.get_vector() + local choices = {} + for idx, word in ipairs(world.raws.language.words) do + table.insert(choices, { + text={ + {text=function() return word.forms.Noun end, width=ENGLISH_COL_WIDTH}, + {gap=2, text=function() return translations[self.subviews.language:getOptionValue()].words[idx].value end, width=NATIVE_COL_WIDTH}, + {text=df.language_part_of_speech[word.part_of_speech], width=15}, + }, + search_key=function() end, + }) + end + return choices +end + +function Rename:refresh_list() end -- @@ -72,9 +304,28 @@ end local function get_unit_target(unit, sync_targets) if not unit then return end + local target = dfhack.units.getVisibleName(unit) local hf = df.historical_figure.find(unit.hist_figure_id) - if hf then table.insert(sync_targets, hf.name) end - return unit.name + if hf then + local hf_name = dfhack.units.getVisibleName(hf) + if hf_name ~= target then + table.insert(sync_targets, hf_name) + end + end + return target +end + +local function get_hf_target(hf, sync_targets) + if not hf then return end + local target = dfhack.units.getVisibleName(hf) + local unit = df.unit.find(hf.unit_id) + if unit then + local unit_name = dfhack.units.getVisibleName(unit) + if unit_name ~= target then + table.insert(sync_targets, unit_name) + end + end + return target end local function get_location_target(site, loc_id) @@ -87,11 +338,8 @@ end local function get_target(opts) local target, sync_targets = nil, {} if opts.histfig_id then - local hf = df.historical_figure.find(opts.histfig_id) - if not hf then qerror('Historical figure not found') end - target = hf.name - local unit = df.unit.find(hf.unit_id) - if unit then table.insert(sync_targets, unit.name) end + target = get_hf_target(df.historical_figure.find(opts.histfig_id), sync_targets) + if not target then qerror('Historical figure not found') end elseif opts.item_id then target = get_artifact_target(df.item.find(opts.item_id)) if not target then qerror('Artifact not found') end From a27c018bb292dbcc5382518aaed9f99bb2306f52 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 1 Jan 2025 19:27:25 -0800 Subject: [PATCH 271/811] implement word loading and editing --- gui/rename.lua | 319 +++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 269 insertions(+), 50 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 2d57cfa999..58ac9d0efc 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -8,11 +8,54 @@ local CH_DN = string.char(31) local ENGLISH_COL_WIDTH = 16 local NATIVE_COL_WIDTH = 16 +local part_of_speech_to_display = { + [df.part_of_speech.Noun] = 'Singular Noun', + [df.part_of_speech.NounPlural] = 'Plural Noun', + [df.part_of_speech.Adjective] = 'Adjective', + [df.part_of_speech.Prefix] = 'Prefix', + [df.part_of_speech.Verb] = 'Present (1st)', + [df.part_of_speech.Verb3rdPerson] = 'Present (3rd)', + [df.part_of_speech.VerbPast] = 'Preterite', + [df.part_of_speech.VerbPassive] = 'Past Participle', + [df.part_of_speech.VerbGerund] = 'Present Participle', +} + +local langauge_name_type_to_category = { + [df.language_name_type.Figure] = {df.language_name_category.Unit}, + [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, + [df.language_name_type.Civilization] = {df.language_name_category.EntityMerchantCompany}, + [df.language_name_type.Squad] = {df.language_name_category.Battle}, + [df.language_name_type.Site] = {df.language_name_category.Keep}, + [df.language_name_type.World] = {df.language_name_category.Region}, + [df.language_name_type.EntitySite] = {df.language_name_category.Keep}, + [df.language_name_type.Temple] = {df.language_name_category.Temple}, + [df.language_name_type.MeadHall] = {df.language_name_category.MeadHall}, + [df.language_name_type.Library] = {df.language_name_category.Library}, + [df.language_name_type.Guildhall] = {df.language_name_category.Guildhall}, + [df.language_name_type.Hospital] = {df.language_name_category.Hospital}, +} + +local language_name_component_to_word_table_index = { + [df.language_name_component.FrontCompound] = df.language_word_table_index.FrontCompound, + [df.language_name_component.RearCompound] = df.language_word_table_index.RearCompound, + [df.language_name_component.FrontCompound] = df.language_word_table_index.FirstName, + [df.language_name_component.FirstAdjective] = df.language_word_table_index.Adjectives, + [df.language_name_component.SecondAdjective] = df.language_word_table_index.Adjectives, + [df.language_name_component.FrontCompound] = df.language_word_table_index.TheX, + [df.language_name_component.FrontCompound] = df.language_word_table_index.OfX, + +} + +local language = df.global.world.raws.language +local translations = df.language_translation.get_vector() + -- -- target selection -- local function select_new_target() + local target, sync_targets = nil, {} + return target, sync_targets end -- @@ -22,14 +65,14 @@ end Rename = defclass(Rename, widgets.Window) Rename.ATTRS { frame_title='Rename', - frame={w=87, h=30}, + frame={w=88, h=31}, resizable=true, - resize_min={w=77, h=30}, + resize_min={w=70, h=31}, } local function get_language_options() local options, max_width = {}, 5 - for idx, lang in ipairs(df.language_translation.get_vector()) do + for idx, lang in ipairs(translations) do max_width = math.max(max_width, #lang.name) table.insert(options, {label=dfhack.capitalizeStringWords(dfhack.lowerCp437(lang.name)), value=idx, pen=COLOR_CYAN}) end @@ -41,21 +84,69 @@ local function pad_text(text, width) end local function sort_by_english_desc(a, b) + if a.data.english ~= b.data.english then + return a.data.english < b.data.english + end + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + if a_native ~= b_native then + return a_native < b_native + end + return a.data.part_of_speech < b.data.part_of_speech end local function sort_by_english_asc(a, b) + if a.data.english ~= b.data.english then + return a.data.english > b.data.english + end + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + if a_native ~= b_native then + return a_native < b_native + end + return a.data.part_of_speech < b.data.part_of_speech end local function sort_by_native_desc(a, b) + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + if a_native ~= b_native then + return a_native < b_native + end + if a.data.english ~= b.data.english then + return a.data.english < b.data.english + end + return a.data.part_of_speech < b.data.part_of_speech end local function sort_by_native_asc(a, b) + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + if a_native ~= b_native then + return a_native > b_native + end + if a.data.english ~= b.data.english then + return a.data.english < b.data.english + end + return a.data.part_of_speech < b.data.part_of_speech end local function sort_by_part_of_speech_desc(a, b) + if a.data.part_of_speech ~= b.data.part_of_speech then + return a.data.part_of_speech < b.data.part_of_speech + end + if a.data.english ~= b.data.english then + return a.data.english < b.data.english + end + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + return a_native < b_native end local function sort_by_part_of_speech_asc(a, b) + if a.data.part_of_speech ~= b.data.part_of_speech then + return a.data.part_of_speech > b.data.part_of_speech + end + if a.data.english ~= b.data.english then + return a.data.english < b.data.english + end + local a_native, b_native = a.data.native_fn(), b.data.native_fn() + return a_native < b_native end function Rename:init(info) @@ -85,7 +176,7 @@ function Rename:init(info) frame={t=0, r=0}, label='Generate random name', key='CUSTOM_CTRL_G', - on_activate=function() end, + on_activate=self:callback('generate_random_name'), auto_width=true, }, widgets.Label{ @@ -146,15 +237,20 @@ function Rename:init(info) }, widgets.Panel{frame={t=2, l=0, w=30}, -- component selector subviews={ - widgets.List{frame={t=0, l=0, b=2}, + widgets.List{ + frame={t=0, l=0, b=2, w=ENGLISH_COL_WIDTH+2}, view_id='component_list', - on_select=function(idx, choice) print('component choice', idx) printall_recurse(choice) end, + on_select=function() if self.subviews.component_list then self:refresh_list() end end, choices=self:get_component_choices(), row_height=2, - scroll_keys={ - SECONDSCROLL_UP = -1, - SECONDSCROLL_DOWN = 1, - }, + scroll_keys={}, + }, + widgets.List{ + frame={t=0, l=ENGLISH_COL_WIDTH+4, b=3}, + on_submit=function(_, choice) choice.data.fn() end, + choices=self:get_component_action_choices(), + cursor_pen=COLOR_CYAN, + scroll_keys={}, }, widgets.HotkeyLabel{ frame={b=1, l=0}, @@ -207,9 +303,9 @@ function Rename:init(info) on_change=self:callback('refresh_list', 'sort_part_of_speech'), }, widgets.FilteredList{ - view_id='list', + view_id='words_list', frame={t=2, l=0, b=0, r=0}, - on_submit=function() end, + on_submit=self:callback('set_component_word'), }, }, }, @@ -218,10 +314,10 @@ function Rename:init(info) } -- replace the FilteredList's built-in EditField with our own - self.subviews.list.list.frame.t = 0 - self.subviews.list.edit.visible = false - self.subviews.list.edit = self.subviews.search - self.subviews.search.on_change = self.subviews.list:callback('onFilterChange') + self.subviews.words_list.list.frame.t = 0 + self.subviews.words_list.edit.visible = false + self.subviews.words_list.edit = self.subviews.search + self.subviews.search.on_change = self.subviews.words_list:callback('onFilterChange') self:refresh_list() end @@ -230,36 +326,161 @@ function Rename:get_component_choices() local choices = {} for val, comp in ipairs(df.language_name_component) do local text = { - {text=comp:gsub('(%l)(%u)', '%1 %2')}, NEWLINE - {text=function() + {text=comp:gsub('(%l)(%u)', '%1 %2')}, NEWLINE, + {gap=2, pen=COLOR_YELLOW, text=function() local word = self.target.words[val] if word < 0 then return end - return ('word: %s'):format(df.global.world.raws.language.words[word].forms.Noun) - end} + return ('%s'):format(language.words[word].forms[self.target.parts_of_speech[val]]) + end}, } table.insert(choices, {text=text, data={val=val}}) end return choices end -function Rename:get_word_choices() - --if self.cache[] - local translations = df.language_translation.get_vector() +function Rename:get_component_action_choices() local choices = {} - for idx, word in ipairs(world.raws.language.words) do - table.insert(choices, { - text={ - {text=function() return word.forms.Noun end, width=ENGLISH_COL_WIDTH}, - {gap=2, text=function() return translations[self.subviews.language:getOptionValue()].words[idx].value end, width=NATIVE_COL_WIDTH}, - {text=df.language_part_of_speech[word.part_of_speech], width=15}, - }, - search_key=function() end, - }) + for val, comp in ipairs(df.language_name_component) do + local randomize_text = {{text='[', pen=COLOR_RED}, 'Random', {text=']', pen=COLOR_RED}} + local randomize_fn = self:callback('randomize_component_word', comp) + table.insert(choices, {text=randomize_text, data={fn=randomize_fn}}) + local clear_text = { + {text=function() return self.target.words[val] >= 0 and '[' or '' end, pen=COLOR_RED}, + {text=function() return self.target.words[val] >= 0 and 'Clear' or '' end }, + {text=function() return self.target.words[val] >= 0 and ']' or '' end, pen=COLOR_RED} + } + local clear_fn = self:callback('clear_component_word', comp) + table.insert(choices, {text=clear_text, data={fn=clear_fn}}) end return choices end -function Rename:refresh_list() +function Rename:clear_component_word(comp) + self.target.words[comp] = -1 + for _, sync_target in ipairs(self.sync_targets) do + sync_target.words[comp] = -1 + end +end + +function Rename:set_component_word(_, choice) + local _, comp_choice = self.subviews.component_list:getSelected() + self.target.words[comp_choice.data.val] = choice.data.idx + self.target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech + for _, sync_target in ipairs(self.sync_targets) do + sync_target.words[comp_choice.data.val] = choice.data.idx + sync_target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech + end +end + +function Rename:randomize_component_word(comp) + local categories = langauge_name_type_to_category[self.target.type] + local category = categories[math.random(#categories)] + local word_table = language.word_table[0][category] + local words = word_table.words[comp] + local idx = math.random(#words)-1 + self.target.words[comp] = words[idx] + self.target.parts_of_speech[comp] = word_table.parts[comp][idx] + for _, sync_target in ipairs(self.sync_targets) do + sync_target.words[comp] = words[idx] + sync_target.parts_of_speech[comp] = word_table.parts[comp][idx] + end +end + +function Rename:generate_random_name() + print('TODO: generate_random_name') +end + +function Rename:add_word_choice(choices, comp, idx, word, part_of_speech) + local english = word.forms[part_of_speech] + if #english == 0 then return end + local function get_native() + return translations[self.subviews.language:getOptionValue()].words[idx].value + end + local part = part_of_speech_to_display[part_of_speech] + local function get_pen() + if idx == self.target.words[comp] and part_of_speech == self.target.parts_of_speech[comp] then + return COLOR_YELLOW + end + end + table.insert(choices, { + text={ + {text=english, width=ENGLISH_COL_WIDTH, pen=get_pen}, + {gap=2, text=get_native, width=NATIVE_COL_WIDTH, pen=get_pen}, + {gap=2, text=part, width=15, pen=get_pen}, + }, + search_key=function() return ('%s %s %s'):format(english, get_native(), part) end, + data={idx=idx, english=english, native_fn=get_native, part_of_speech=part_of_speech}, + }) +end + +function Rename:get_word_choices(comp) + if self.cache[comp] then + return self.cache[comp] + end + + local choices = {} + for idx, word in ipairs(language.words) do + local flags = word.flags + if comp == df.language_name_component.FrontCompound then + if flags.front_compound_noun_sing then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Noun) end + if flags.front_compound_noun_plur then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.NounPlural) end + if flags.front_compound_adj then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Adjective) end + if flags.front_compound_prefix then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Prefix) end + if flags.standard_verb then + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Verb) + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.VerbPassive) + end + elseif comp == df.language_name_component.RearCompound then + if flags.rear_compound_noun_sing then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Noun) end + if flags.rear_compound_noun_plur then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.NounPlural) end + if flags.rear_compound_adj then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Adjective) end + if flags.standard_verb then + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Verb) + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Verb3rdPerson) + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.VerbPast) + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.VerbPassive) + end + elseif comp == df.language_name_component.FirstAdjective or comp == df.language_name_component.SecondAdjective then + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Adjective) + elseif comp == df.language_name_component.HyphenCompound then + if flags.the_compound_noun_sing then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Noun) end + if flags.the_compound_noun_plur then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.NounPlural) end + if flags.the_compound_adj then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Adjective) end + if flags.the_compound_prefix then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Prefix) end + elseif comp == df.language_name_component.TheX then + if flags.the_noun_sing then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Noun) end + if flags.the_noun_plur then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.NounPlural) end + elseif comp == df.language_name_component.OfX then + if flags.of_noun_sing then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.Noun) end + if flags.of_noun_plur then self:add_word_choice(choices, comp, idx, word, df.part_of_speech.NounPlural) end + if flags.standard_verb then + self:add_word_choice(choices, comp, idx, word, df.part_of_speech.VerbGerund) + end + end + end + + self.cache[comp] = choices + return choices +end + +function Rename:refresh_list(sort_widget) + sort_widget = sort_widget or 'sort' + sort_fn = self.subviews.sort:getOptionValue() + if sort_fn == DEFAULT_NIL then + self.subviews[sort_widget]:cycle() + return + end + for _,widget_name in ipairs{'sort', 'sort_english', 'sort_native', 'sort_part_of_speech'} do + self.subviews[widget_name]:setOption(sort_fn) + end + local list = self.subviews.words_list + local saved_filter = list:getFilter() + list:setFilter('') + local _, comp_choice = self.subviews.component_list:getSelected() + local choices = self:get_word_choices(comp_choice.data.val) + table.sort(choices, self.subviews.sort:getOptionValue()) + list:setChoices(choices) + list:setFilter(saved_filter) end -- @@ -302,30 +523,23 @@ local function get_artifact_target(item) return rec.name end -local function get_unit_target(unit, sync_targets) - if not unit then return end - local target = dfhack.units.getVisibleName(unit) - local hf = df.historical_figure.find(unit.hist_figure_id) - if hf then - local hf_name = dfhack.units.getVisibleName(hf) - if hf_name ~= target then - table.insert(sync_targets, hf_name) - end - end - return target -end - -local function get_hf_target(hf, sync_targets) +local function get_hf_target(hf) if not hf then return end local target = dfhack.units.getVisibleName(hf) local unit = df.unit.find(hf.unit_id) + local sync_targets = {} if unit then local unit_name = dfhack.units.getVisibleName(unit) if unit_name ~= target then table.insert(sync_targets, unit_name) end end - return target + return target, sync_targets +end + +local function get_unit_target(unit) + if not unit then return end + return get_hf_target(df.historical_figure.find(unit.hist_figure_id)) end local function get_location_target(site, loc_id) @@ -338,7 +552,7 @@ end local function get_target(opts) local target, sync_targets = nil, {} if opts.histfig_id then - target = get_hf_target(df.historical_figure.find(opts.histfig_id), sync_targets) + target, sync_targets = get_hf_target(df.historical_figure.find(opts.histfig_id)) if not target then qerror('Historical figure not found') end elseif opts.item_id then target = get_artifact_target(df.item.find(opts.item_id)) @@ -357,7 +571,7 @@ local function get_target(opts) if not squad then qerror('Squad not found') end target = squad.name elseif opts.unit_id then - target = get_unit_target(df.unit.find(opts.unit_id), sync_targets) + target, sync_targets = get_unit_target(df.unit.find(opts.unit_id)) if not target then qerror('Unit not found') end elseif opts.world then target = df.global.world.world_data.name @@ -407,6 +621,11 @@ if not target then target = get_artifact_target(item) elseif zone then target = get_location_target(df.world_site.find(zone.site_id), zone.location_id) + else + target, sync_targets = select_new_target() + end + if not target then + qerror('No target selected') end end From 444f56b2ab4c5aacd6df23809d672abd78ef3497 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 1 Jan 2025 20:50:22 -0800 Subject: [PATCH 272/811] reorder a bit, support units with no hf --- gui/rename.lua | 83 ++++++++++++++++++++++++++------------------------ 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 58ac9d0efc..79d5a39d70 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -8,44 +8,6 @@ local CH_DN = string.char(31) local ENGLISH_COL_WIDTH = 16 local NATIVE_COL_WIDTH = 16 -local part_of_speech_to_display = { - [df.part_of_speech.Noun] = 'Singular Noun', - [df.part_of_speech.NounPlural] = 'Plural Noun', - [df.part_of_speech.Adjective] = 'Adjective', - [df.part_of_speech.Prefix] = 'Prefix', - [df.part_of_speech.Verb] = 'Present (1st)', - [df.part_of_speech.Verb3rdPerson] = 'Present (3rd)', - [df.part_of_speech.VerbPast] = 'Preterite', - [df.part_of_speech.VerbPassive] = 'Past Participle', - [df.part_of_speech.VerbGerund] = 'Present Participle', -} - -local langauge_name_type_to_category = { - [df.language_name_type.Figure] = {df.language_name_category.Unit}, - [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, - [df.language_name_type.Civilization] = {df.language_name_category.EntityMerchantCompany}, - [df.language_name_type.Squad] = {df.language_name_category.Battle}, - [df.language_name_type.Site] = {df.language_name_category.Keep}, - [df.language_name_type.World] = {df.language_name_category.Region}, - [df.language_name_type.EntitySite] = {df.language_name_category.Keep}, - [df.language_name_type.Temple] = {df.language_name_category.Temple}, - [df.language_name_type.MeadHall] = {df.language_name_category.MeadHall}, - [df.language_name_type.Library] = {df.language_name_category.Library}, - [df.language_name_type.Guildhall] = {df.language_name_category.Guildhall}, - [df.language_name_type.Hospital] = {df.language_name_category.Hospital}, -} - -local language_name_component_to_word_table_index = { - [df.language_name_component.FrontCompound] = df.language_word_table_index.FrontCompound, - [df.language_name_component.RearCompound] = df.language_word_table_index.RearCompound, - [df.language_name_component.FrontCompound] = df.language_word_table_index.FirstName, - [df.language_name_component.FirstAdjective] = df.language_word_table_index.Adjectives, - [df.language_name_component.SecondAdjective] = df.language_word_table_index.Adjectives, - [df.language_name_component.FrontCompound] = df.language_word_table_index.TheX, - [df.language_name_component.FrontCompound] = df.language_word_table_index.OfX, - -} - local language = df.global.world.raws.language local translations = df.language_translation.get_vector() @@ -372,6 +334,32 @@ function Rename:set_component_word(_, choice) end end +local langauge_name_type_to_category = { + [df.language_name_type.Figure] = {df.language_name_category.Unit}, + [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, + [df.language_name_type.Civilization] = {df.language_name_category.EntityMerchantCompany}, + [df.language_name_type.Squad] = {df.language_name_category.Battle}, + [df.language_name_type.Site] = {df.language_name_category.Keep}, + [df.language_name_type.World] = {df.language_name_category.Region}, + [df.language_name_type.EntitySite] = {df.language_name_category.Keep}, + [df.language_name_type.Temple] = {df.language_name_category.Temple}, + [df.language_name_type.MeadHall] = {df.language_name_category.MeadHall}, + [df.language_name_type.Library] = {df.language_name_category.Library}, + [df.language_name_type.Guildhall] = {df.language_name_category.Guildhall}, + [df.language_name_type.Hospital] = {df.language_name_category.Hospital}, +} + +local language_name_component_to_word_table_index = { + [df.language_name_component.FrontCompound] = df.language_word_table_index.FrontCompound, + [df.language_name_component.RearCompound] = df.language_word_table_index.RearCompound, + [df.language_name_component.FrontCompound] = df.language_word_table_index.FirstName, + [df.language_name_component.FirstAdjective] = df.language_word_table_index.Adjectives, + [df.language_name_component.SecondAdjective] = df.language_word_table_index.Adjectives, + [df.language_name_component.FrontCompound] = df.language_word_table_index.TheX, + [df.language_name_component.FrontCompound] = df.language_word_table_index.OfX, + +} + function Rename:randomize_component_word(comp) local categories = langauge_name_type_to_category[self.target.type] local category = categories[math.random(#categories)] @@ -390,6 +378,18 @@ function Rename:generate_random_name() print('TODO: generate_random_name') end +local part_of_speech_to_display = { + [df.part_of_speech.Noun] = 'Singular Noun', + [df.part_of_speech.NounPlural] = 'Plural Noun', + [df.part_of_speech.Adjective] = 'Adjective', + [df.part_of_speech.Prefix] = 'Prefix', + [df.part_of_speech.Verb] = 'Present (1st)', + [df.part_of_speech.Verb3rdPerson] = 'Present (3rd)', + [df.part_of_speech.VerbPast] = 'Preterite', + [df.part_of_speech.VerbPassive] = 'Past Participle', + [df.part_of_speech.VerbGerund] = 'Present Participle', +} + function Rename:add_word_choice(choices, comp, idx, word, part_of_speech) local english = word.forms[part_of_speech] if #english == 0 then return end @@ -539,7 +539,12 @@ end local function get_unit_target(unit) if not unit then return end - return get_hf_target(df.historical_figure.find(unit.hist_figure_id)) + local hf = df.historical_figure.find(unit.hist_figure_id) + if hf then + return get_hf_target(hf) + end + -- unit with no hf + return dfhack.units.getVisibleName(unit), {} end local function get_location_target(site, loc_id) From 3bb6321166d46967b4b61d8c67f72351ae56abe3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 1 Jan 2025 22:16:37 -0800 Subject: [PATCH 273/811] target loading, per-component keyboard hotkeys --- gui/rename.lua | 322 +++++++++++++++++++++++++++++++++--------------- gui/sitemap.lua | 9 +- 2 files changed, 232 insertions(+), 99 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 79d5a39d70..d5e0850784 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -1,5 +1,7 @@ local argparse = require('argparse') +local dlg = require('gui.dialogs') local gui = require('gui') +local sitemap = reqscript('gui/sitemap') local utils = require('utils') local widgets = require('gui.widgets') @@ -15,11 +17,143 @@ local translations = df.language_translation.get_vector() -- target selection -- -local function select_new_target() - local target, sync_targets = nil, {} +local function get_artifact_target(item) + if not item or not item.flags.artifact then return end + local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) + if not gref then return end + local rec = df.artifact_record.find(gref.artifact_id) + if not rec then return end + return rec.name +end + +local function get_hf_target(hf) + if not hf then return end + local target = dfhack.units.getVisibleName(hf) + local unit = df.unit.find(hf.unit_id) + local sync_targets = {} + if unit then + local unit_name = dfhack.units.getVisibleName(unit) + if unit_name ~= target then + table.insert(sync_targets, unit_name) + end + end return target, sync_targets end +local function get_unit_target(unit) + if not unit then return end + local hf = df.historical_figure.find(unit.hist_figure_id) + if hf then + return get_hf_target(hf) + end + -- unit with no hf + return dfhack.units.getVisibleName(unit), {} +end + +local function get_location_target(site, loc_id) + if not site or loc_id < 0 then return end + local loc = utils.binsearch(site.buildings, loc_id, 'id') + if not loc then return end + return loc.name +end + +local function select_artifact(cb) + local choices = {} + for _, item in ipairs(df.global.world.items.other.ANY_ARTIFACT) do + if item.flags.garbage_collect then goto continue end + local target = get_artifact_target(item) + if not target then goto continue end + table.insert(choices, { + text=dfhack.items.getReadableDescription(item), + data={target=target}, + }) + ::continue:: + end + dlg.showListPrompt('Rename', 'Select an artifact to rename:', COLOR_WHITE, + choices, function(_, choice) cb(choice.data.target) end, nil, nil, true) +end + +local function select_location(site, cb) + local choices = {} + for _,loc in ipairs(site.buildings) do + local desc, pen = sitemap.get_location_desc(loc) + table.insert(choices, { + text={ + dfhack.TranslateName(loc.name, true), + ' (', + {text=desc, pen=pen}, + ')', + }, + data={target=loc.name}, + }) + end + dlg.showListPrompt('Rename', 'Select a location to rename:', COLOR_WHITE, + choices, function(_, choice) cb(choice.data.target) end, nil, nil, true) +end + +local function select_site(site, cb) + cb(site.name) +end + +local function select_squad(fort, cb) + local choices = {} + for _,squad_id in ipairs(fort.squads) do + local squad = df.squad.find(squad_id) + if squad then + table.insert(choices, { + text=dfhack.military.getSquadName(squad.id), + data={target=squad.name}, + }) + end + end + dlg.showListPrompt('Rename', 'Select a squad to rename:', COLOR_WHITE, + choices, function(_, choice) cb(choice.data.target) end, nil, nil, true) +end + +local function select_unit(cb) + local choices = {} + for _,unit in ipairs(df.global.world.units.active) do + local target, sync_targets = get_unit_target(unit) + if target then + table.insert(choices, { + text=dfhack.units.getReadableName(unit), + data={target=target, sync_targets=sync_targets}, + }) + end + end + dlg.showListPrompt('Rename', 'Select a unit to rename:', COLOR_WHITE, + choices, function(_, choice) cb(choice.data.target, choice.data.sync_targets) end, + nil, nil, true) +end + +local function select_world(cb) + cb(df.global.world.world_data.name) +end + +local function select_new_target(cb) + local choices = {} + if #df.global.world.items.other.ANY_ARTIFACT > 0 then + table.insert(choices, {text='An artifact', data={fn=select_artifact}}) + end + local site = dfhack.world.getCurrentSite() + if site then + if #site.buildings > 0 then + table.insert(choices, {text='A location', data={fn=curry(select_location, site)}}) + end + table.insert(choices, {text='This fortress', data={fn=curry(select_site, site)}}) + local fort = df.historical_entity.find(df.global.plotinfo.group_id) + if fort and #fort.squads > 0 then + table.insert(choices, {text='A squad', data={fn=curry(select_squad, fort)}}) + end + end + if #df.global.world.units.active > 0 then + table.insert(choices, {text='A unit', data={fn=select_unit}}) + end + table.insert(choices, {text='This world', data={fn=select_world}}) + dlg.showListPrompt('Rename', 'What would you like to rename?', COLOR_WHITE, + choices, function(_, choice) choice.data.fn(cb) end) +end + -- -- Rename -- @@ -27,9 +161,9 @@ end Rename = defclass(Rename, widgets.Window) Rename.ATTRS { frame_title='Rename', - frame={w=88, h=31}, + frame={w=89, h=33}, resizable=true, - resize_min={w=70, h=31}, + resize_min={w=61}, } local function get_language_options() @@ -125,21 +259,22 @@ function Rename:init(info) frame={t=0, l=0}, key='CUSTOM_CTRL_N', label='Select new target', + auto_width=true, on_activate=function() - local target, sync_targets = select_new_target() - if target then - self.target, self.sync_targets = target, sync_targets + select_new_target(function(target, sync_targets) + if not target then return end + self.target, self.sync_targets = target, sync_targets or {} self.subviews.language:setOption(self.target.language) - end + end) end, visible=info.show_selector, }, widgets.HotkeyLabel{ frame={t=0, r=0}, - label='Generate random name', key='CUSTOM_CTRL_G', - on_activate=self:callback('generate_random_name'), + label='Generate random name', auto_width=true, + on_activate=self:callback('generate_random_name'), }, widgets.Label{ frame={t=2}, @@ -215,20 +350,38 @@ function Rename:init(info) scroll_keys={}, }, widgets.HotkeyLabel{ - frame={b=1, l=0}, + frame={b=3, l=0}, key='SECONDSCROLL_UP', label='Prev component', on_activate=function() self.subviews.component_list:moveCursor(-1) end, }, widgets.HotkeyLabel{ - frame={b=0, l=0}, + frame={b=2, l=0}, key='SECONDSCROLL_DOWN', label='Next component', on_activate=function() self.subviews.component_list:moveCursor(1) end, }, + widgets.HotkeyLabel{ + frame={b=1, l=0}, + key='CUSTOM_CTRL_D', + label='Randomize component', + on_activate=function() + local _, comp_choice = self.subviews.component_list:getSelected() + self:randomize_component_word(comp_choice.data.val) + end, + }, + widgets.HotkeyLabel{ + frame={b=0, l=0}, + key='CUSTOM_CTRL_H', + label='Clear component', + on_activate=function() + local _, comp_choice = self.subviews.component_list:getSelected() + self:clear_component_word(comp_choice.data.val) + end, + }, }, }, - widgets.Panel{frame={t=2, l=30}, -- words table + widgets.Panel{frame={t=2, l=31}, -- words table subviews={ widgets.CycleHotkeyLabel{ view_id='sort_english', @@ -463,9 +616,9 @@ function Rename:get_word_choices(comp) return choices end -function Rename:refresh_list(sort_widget) +function Rename:refresh_list(sort_widget, sort_fn) sort_widget = sort_widget or 'sort' - sort_fn = self.subviews.sort:getOptionValue() + sort_fn = sort_fn or self.subviews.sort:getOptionValue() if sort_fn == DEFAULT_NIL then self.subviews[sort_widget]:cycle() return @@ -496,7 +649,7 @@ function RenameScreen:init(info) self:addviews{ Rename{ target=info.target, - sync_targets=info.sync_targets, + sync_targets=info.sync_targets or {}, show_selector=info.show_selector, } } @@ -514,46 +667,6 @@ if not dfhack.isWorldLoaded() then qerror('This script requires a world to be loaded') end -local function get_artifact_target(item) - if not item or not item.flags.artifact then return end - local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) - if not gref then return end - local rec = df.artifact_record.find(gref.artifact_id) - if not rec then return end - return rec.name -end - -local function get_hf_target(hf) - if not hf then return end - local target = dfhack.units.getVisibleName(hf) - local unit = df.unit.find(hf.unit_id) - local sync_targets = {} - if unit then - local unit_name = dfhack.units.getVisibleName(unit) - if unit_name ~= target then - table.insert(sync_targets, unit_name) - end - end - return target, sync_targets -end - -local function get_unit_target(unit) - if not unit then return end - local hf = df.historical_figure.find(unit.hist_figure_id) - if hf then - return get_hf_target(hf) - end - -- unit with no hf - return dfhack.units.getVisibleName(unit), {} -end - -local function get_location_target(site, loc_id) - if not site or loc_id < 0 then return end - local loc = utils.binsearch(site.buildings, loc_id, 'id') - if not loc then return end - return loc.name -end - local function get_target(opts) local target, sync_targets = nil, {} if opts.histfig_id then @@ -584,58 +697,71 @@ local function get_target(opts) return target, sync_targets end -local opts = { - help=false, - entity_id=nil, - histfig_id=nil, - item_id=nil, - location_id=nil, - site_id=nil, - squad_id=nil, - unit_id=nil, - world=false, - show_selector=true, -} -local positionals = argparse.processArgsGetopt({...}, { - { 'a', 'artifact', handler=function(optarg) opts.item_id = argparse.nonnegativeInt(optarg, 'artifact') end }, - { 'e', 'entity', handler=function(optarg) opts.entity_id = argparse.nonnegativeInt(optarg, 'entity') end }, - { 'f', 'histfig', handler=function(optarg) opts.histfig_id = argparse.nonnegativeInt(optarg, 'histfig') end }, - { 'h', 'help', handler = function() opts.help = true end }, - { 'l', 'location', handler=function(optarg) opts.location_id = argparse.nonnegativeInt(optarg, 'location') end }, - { 'q', 'squad', handler=function(optarg) opts.squad_id = argparse.nonnegativeInt(optarg, 'squad') end }, - { 's', 'site', handler=function(optarg) opts.site_id = argparse.nonnegativeInt(optarg, 'site') end }, - { 'u', 'unit', handler=function(optarg) opts.unit_id = argparse.nonnegativeInt(optarg, 'unit') end }, - { 'w', 'world', handler=function() opts.world = true end }, - { '', 'no-target-selector', handler=function() opts.show_selector = false end }, -}) - -if opts.help or positionals[1] == 'help' then - print(dfhack.script_help()) - return -end - -local target, sync_targets = get_target(opts) - -if not target then +local function main(args) + local opts = { + help=false, + entity_id=nil, + histfig_id=nil, + item_id=nil, + location_id=nil, + site_id=nil, + squad_id=nil, + unit_id=nil, + world=false, + show_selector=true, + } + local positionals = argparse.processArgsGetopt(args, { + { 'a', 'artifact', handler=function(optarg) opts.item_id = argparse.nonnegativeInt(optarg, 'artifact') end }, + { 'e', 'entity', handler=function(optarg) opts.entity_id = argparse.nonnegativeInt(optarg, 'entity') end }, + { 'f', 'histfig', handler=function(optarg) opts.histfig_id = argparse.nonnegativeInt(optarg, 'histfig') end }, + { 'h', 'help', handler = function() opts.help = true end }, + { 'l', 'location', handler=function(optarg) opts.location_id = argparse.nonnegativeInt(optarg, 'location') end }, + { 'q', 'squad', handler=function(optarg) opts.squad_id = argparse.nonnegativeInt(optarg, 'squad') end }, + { 's', 'site', handler=function(optarg) opts.site_id = argparse.nonnegativeInt(optarg, 'site') end }, + { 'u', 'unit', handler=function(optarg) opts.unit_id = argparse.nonnegativeInt(optarg, 'unit') end }, + { 'w', 'world', handler=function() opts.world = true end }, + { '', 'no-target-selector', handler=function() opts.show_selector = false end }, + }) + + if opts.help or positionals[1] == 'help' then + print(dfhack.script_help()) + return + end + + local function launch(target, sync_targets) + view = view and view:raise() or RenameScreen{ + target=target, + sync_targets=sync_targets, + show_selector=opts.show_selector, + }:show() + end + + local target, sync_targets = get_target(opts) + if target then + launch(target, sync_targets) + return + end + local unit = dfhack.gui.getSelectedUnit(true) local item = dfhack.gui.getSelectedItem(true) local zone = dfhack.gui.getSelectedCivZone(true) if unit then - target = get_unit_target(unit, sync_targets) + target, sync_targets = get_unit_target(unit) elseif item then target = get_artifact_target(item) elseif zone then target = get_location_target(df.world_site.find(zone.site_id), zone.location_id) - else - target, sync_targets = select_new_target() end - if not target then + if target then + launch(target, sync_targets) + return + end + + if not opts.show_selector then qerror('No target selected') end + + select_new_target(launch) end -view = view and view:raise() or RenameScreen{ - target=target, - sync_targets=sync_targets, - show_selector=opts.show_selector -}:show() +main{...} diff --git a/gui/sitemap.lua b/gui/sitemap.lua index 4440cdd730..ec7ffff7d1 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -1,3 +1,5 @@ +--@ module = true + local gui = require('gui') local utils = require('utils') local widgets = require('gui.widgets') @@ -18,7 +20,8 @@ local function to_title_case(str) return dfhack.capitalizeStringWords(dfhack.lowerCp437(str:gsub('_', ' '))) end -local function get_location_desc(loc) +-- also called by gui/rename +function get_location_desc(loc) if df.abstract_building_hospitalst:is_instance(loc) then return 'Hospital', COLOR_WHITE elseif df.abstract_building_inn_tavernst:is_instance(loc) then @@ -309,6 +312,10 @@ function SitemapScreen:onDismiss() view = nil end +if dfhack_flags.module then + return +end + if not dfhack.isMapLoaded() then qerror('This script requires a map to be loaded') end From 6de7797c82a83a72ff0ebf99a18a9cee98382942 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 00:55:30 -0800 Subject: [PATCH 274/811] allow editing of first name for units --- gui/rename.lua | 185 ++++++++++++++++++++++++++++++++++++------------- 1 file changed, 138 insertions(+), 47 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index d5e0850784..ea1bcc01a8 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -1,3 +1,5 @@ +--@module = true + local argparse = require('argparse') local dlg = require('gui.dialogs') local gui = require('gui') @@ -161,7 +163,7 @@ end Rename = defclass(Rename, widgets.Window) Rename.ATTRS { frame_title='Rename', - frame={w=89, h=33}, + frame={w=89, h=43}, resizable=true, resize_min={w=61}, } @@ -291,12 +293,7 @@ function Rename:init(info) label='Language:', options=language_options, initial_option=self.target and self.target.language or 0, - on_change=function(val) - self.target.language = val - for _, sync_target in ipairs(self.sync_targets) do - sync_target.language = val - end - end, + on_change=self:callback('set_language'), }, widgets.Label{ frame={t=6, l=7}, @@ -304,46 +301,29 @@ function Rename:init(info) }, }, }, + widgets.Divider{frame={t=8, l=29, w=1}, + frame_style=gui.FRAME_THIN, + frame_style_t=false, + frame_style_b=false, + }, widgets.Panel{frame={t=8}, -- body subviews={ - widgets.Panel{frame={t=0, h=1}, -- toolbar + widgets.Panel{frame={t=0, l=0, w=30}, -- component selector subviews={ - widgets.CycleHotkeyLabel{ - view_id='sort', - frame={t=0, l=0, w=32}, - label='Sort by:', - key='CUSTOM_CTRL_O', - options={ - {label='English'..CH_DN, value=sort_by_english_desc}, - {label='English'..CH_UP, value=sort_by_english_asc}, - {label='native'..CH_DN, value=sort_by_native_desc}, - {label='native'..CH_UP, value=sort_by_native_asc}, - {label='part of speech'..CH_DN, value=sort_by_part_of_speech_desc}, - {label='part of speech'..CH_UP, value=sort_by_part_of_speech_asc}, - }, - initial_option=sort_by_english_desc, - on_change=self:callback('refresh_list', 'sort'), - }, - widgets.EditField{ - view_id='search', - frame={t=0, l=35}, - label_text='Search: ', - ignore_keys={'SECONDSCROLL_DOWN', 'SECONDSCROLL_UP'} + widgets.Label{ + frame={t=0, l=0}, + text='Name components:', }, - }, - }, - widgets.Panel{frame={t=2, l=0, w=30}, -- component selector - subviews={ widgets.List{ - frame={t=0, l=0, b=2, w=ENGLISH_COL_WIDTH+2}, + frame={t=2, l=0, b=4, w=ENGLISH_COL_WIDTH+2}, view_id='component_list', - on_select=function() if self.subviews.component_list then self:refresh_list() end end, + on_select=self:callback('refresh_list'), choices=self:get_component_choices(), - row_height=2, + row_height=3, scroll_keys={}, }, widgets.List{ - frame={t=0, l=ENGLISH_COL_WIDTH+4, b=3}, + frame={t=2, l=ENGLISH_COL_WIDTH+4, b=4}, on_submit=function(_, choice) choice.data.fn() end, choices=self:get_component_action_choices(), cursor_pen=COLOR_CYAN, @@ -353,13 +333,23 @@ function Rename:init(info) frame={b=3, l=0}, key='SECONDSCROLL_UP', label='Prev component', - on_activate=function() self.subviews.component_list:moveCursor(-1) end, + on_activate=function() + local clist = self.subviews.component_list + local move = self.target.type ~= df.language_name_type.Figure and + clist:getSelected() == 2 and #clist:getChoices()-2 or -1 + self.subviews.component_list:moveCursor(move) + end, }, widgets.HotkeyLabel{ frame={b=2, l=0}, key='SECONDSCROLL_DOWN', label='Next component', - on_activate=function() self.subviews.component_list:moveCursor(1) end, + on_activate=function() + local clist = self.subviews.component_list + local move = self.target.type ~= df.language_name_type.Figure and + clist:getSelected() == #clist:getChoices() and -#clist:getChoices()+2 or 1 + self.subviews.component_list:moveCursor(move) + end, }, widgets.HotkeyLabel{ frame={b=1, l=0}, @@ -367,7 +357,11 @@ function Rename:init(info) label='Randomize component', on_activate=function() local _, comp_choice = self.subviews.component_list:getSelected() - self:randomize_component_word(comp_choice.data.val) + if comp_choice.data.is_first_name then + self:randomize_first_name() + else + self:randomize_component_word(comp_choice.data.val) + end end, }, widgets.HotkeyLabel{ @@ -378,14 +372,41 @@ function Rename:init(info) local _, comp_choice = self.subviews.component_list:getSelected() self:clear_component_word(comp_choice.data.val) end, + enabled=function() + local _, comp_choice = self.subviews.component_list:getSelected() + if comp_choice.data.is_first_name then return false end + return self.target.words[comp_choice.data.val] >= 0 + end, }, }, }, - widgets.Panel{frame={t=2, l=31}, -- words table + widgets.Panel{frame={t=0, l=31}, -- words table subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={t=0, l=0, w=19}, + label='Change sort', + key='CUSTOM_CTRL_O', + options={ + {label='', value=sort_by_english_desc}, + {label='', value=sort_by_english_asc}, + {label='', value=sort_by_native_desc}, + {label='', value=sort_by_native_asc}, + {label='', value=sort_by_part_of_speech_desc}, + {label='', value=sort_by_part_of_speech_asc}, + }, + initial_option=sort_by_english_desc, + on_change=self:callback('refresh_list', 'sort'), + }, + widgets.EditField{ + view_id='search', + frame={t=0, l=22}, + label_text='Search: ', + ignore_keys={'SECONDSCROLL_DOWN', 'SECONDSCROLL_UP'} + }, widgets.CycleHotkeyLabel{ view_id='sort_english', - frame={t=0, l=0, w=8}, + frame={t=2, l=0, w=8}, options={ {label='English', value=DEFAULT_NIL}, {label='English'..CH_DN, value=sort_by_english_desc}, @@ -397,7 +418,7 @@ function Rename:init(info) }, widgets.CycleHotkeyLabel{ view_id='sort_native', - frame={t=0, l=ENGLISH_COL_WIDTH+2, w=7}, + frame={t=2, l=ENGLISH_COL_WIDTH+2, w=7}, options={ {label='native', value=DEFAULT_NIL}, {label='native'..CH_DN, value=sort_by_native_desc}, @@ -408,7 +429,7 @@ function Rename:init(info) }, widgets.CycleHotkeyLabel{ view_id='sort_part_of_speech', - frame={t=0, l=ENGLISH_COL_WIDTH+2+NATIVE_COL_WIDTH+2, w=15}, + frame={t=2, l=ENGLISH_COL_WIDTH+2+NATIVE_COL_WIDTH+2, w=15}, options={ {label='part of speech', value=DEFAULT_NIL}, {label='part_of_speech'..CH_DN, value=sort_by_part_of_speech_desc}, @@ -419,7 +440,7 @@ function Rename:init(info) }, widgets.FilteredList{ view_id='words_list', - frame={t=2, l=0, b=0, r=0}, + frame={t=4, l=0, b=0, r=0}, on_submit=self:callback('set_component_word'), }, }, @@ -439,6 +460,14 @@ end function Rename:get_component_choices() local choices = {} + table.insert(choices, { + text={ + {text='First Name', + pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or nil end}, + NEWLINE, + {gap=2, pen=COLOR_YELLOW, text=function() return self.target.first_name end} + }, + data={val=df.language_name_component.TheX, is_first_name=true}}) for val, comp in ipairs(df.language_name_component) do local text = { {text=comp:gsub('(%l)(%u)', '%1 %2')}, NEWLINE, @@ -455,8 +484,19 @@ end function Rename:get_component_action_choices() local choices = {} + table.insert(choices, { + text={ + {text='[', pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or COLOR_RED end}, + {text='Random', pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or nil end}, + {text=']', pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or COLOR_RED end} + }, + data={fn=self:callback('randomize_first_name')}, + }) + table.insert(choices, {text='', data={fn=function() end}}) -- shouldn't be able to clear a first name, only overwrite + table.insert(choices, {text='', data={fn=function() end}}) + + local randomize_text = {{text='[', pen=COLOR_RED}, 'Random', {text=']', pen=COLOR_RED}} for val, comp in ipairs(df.language_name_component) do - local randomize_text = {{text='[', pen=COLOR_RED}, 'Random', {text=']', pen=COLOR_RED}} local randomize_fn = self:callback('randomize_component_word', comp) table.insert(choices, {text=randomize_text, data={fn=randomize_fn}}) local clear_text = { @@ -466,6 +506,7 @@ function Rename:get_component_action_choices() } local clear_fn = self:callback('clear_component_word', comp) table.insert(choices, {text=clear_text, data={fn=clear_fn}}) + table.insert(choices, {text='', data={fn=function() end}}) end return choices end @@ -477,8 +518,19 @@ function Rename:clear_component_word(comp) end end +function Rename:set_first_name(choice) + self.target.first_name = translations[self.subviews.language:getOptionValue()].words[choice.data.idx].value + for _, sync_target in ipairs(self.sync_targets) do + sync_target.first_name = self.target.first_name + end +end + function Rename:set_component_word(_, choice) local _, comp_choice = self.subviews.component_list:getSelected() + if comp_choice.data.is_first_name then + self:set_first_name(choice) + return + end self.target.words[comp_choice.data.val] = choice.data.idx self.target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech for _, sync_target in ipairs(self.sync_targets) do @@ -487,6 +539,17 @@ function Rename:set_component_word(_, choice) end end +function Rename:set_language(val, prev_val) + self.target.language = val + -- translate current first name into target language + local idx = utils.linear_index(translations[prev_val].words, self.target.first_name, 'value') + if idx then self.target.first_name = translations[val].words[idx].value end + for _, sync_target in ipairs(self.sync_targets) do + sync_target.language = val + sync_target.first_name = self.target.first_name + end +end + local langauge_name_type_to_category = { [df.language_name_type.Figure] = {df.language_name_category.Unit}, [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, @@ -513,6 +576,12 @@ local language_name_component_to_word_table_index = { } +function Rename:randomize_first_name() + if self.target.type ~= df.language_name_type.Figure then return end + local choices = self:get_word_choices(df.language_name_component.TheX) + self:set_first_name(choices[math.random(#choices)]) +end + function Rename:randomize_component_word(comp) local categories = langauge_name_type_to_category[self.target.type] local category = categories[math.random(#categories)] @@ -550,7 +619,12 @@ function Rename:add_word_choice(choices, comp, idx, word, part_of_speech) return translations[self.subviews.language:getOptionValue()].words[idx].value end local part = part_of_speech_to_display[part_of_speech] + local clist = self.subviews.component_list local function get_pen() + local _, comp_choice = clist:getSelected() + if comp_choice.data.is_first_name then + return get_native() == self.target.first_name and COLOR_YELLOW or nil + end if idx == self.target.words[comp] and part_of_speech == self.target.parts_of_speech[comp] then return COLOR_YELLOW end @@ -617,6 +691,13 @@ function Rename:get_word_choices(comp) end function Rename:refresh_list(sort_widget, sort_fn) + local clist = self.subviews.component_list + if not clist then return end + if self.target.type ~= df.language_name_type.Figure and clist:getSelected() == 1 then + clist:setSelected(self.prev_selected_component or 2) + end + self.prev_selected_component = clist:getSelected() + sort_widget = sort_widget or 'sort' sort_fn = sort_fn or self.subviews.sort:getOptionValue() if sort_fn == DEFAULT_NIL then @@ -629,7 +710,7 @@ function Rename:refresh_list(sort_widget, sort_fn) local list = self.subviews.words_list local saved_filter = list:getFilter() list:setFilter('') - local _, comp_choice = self.subviews.component_list:getSelected() + local _, comp_choice = clist:getSelected() local choices = self:get_word_choices(comp_choice.data.val) table.sort(choices, self.subviews.sort:getOptionValue()) list:setChoices(choices) @@ -659,10 +740,20 @@ function RenameScreen:onDismiss() view = nil end +-- +-- Overlays +-- + +OVERLAY_WIDGETS = {} + -- -- CLI -- +if dfhack_flags.module then + return +end + if not dfhack.isWorldLoaded() then qerror('This script requires a world to be loaded') end From 7b7ae031cddcafa8225de62311894d49bf802d2e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 01:33:26 -0800 Subject: [PATCH 275/811] implement savegame world renaming --- gui/rename.lua | 95 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 76 insertions(+), 19 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index ea1bcc01a8..b18e0a13cc 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -3,6 +3,7 @@ local argparse = require('argparse') local dlg = require('gui.dialogs') local gui = require('gui') +local overlay = require('plugins.overlay') local sitemap = reqscript('gui/sitemap') local utils = require('utils') local widgets = require('gui.widgets') @@ -59,6 +60,17 @@ local function get_location_target(site, loc_id) return loc.name end +local function get_world_target() + local target = df.global.world.world_data.name + local sync_targets = { + function() + df.global.world.cur_savegame.world_header.world_name = + ('%s, "%s"'):format(dfhack.TranslateName(target), dfhack.TranslateName(target, true)) + end + } + return target, sync_targets +end + local function select_artifact(cb) local choices = {} for _, item in ipairs(df.global.world.items.other.ANY_ARTIFACT) do @@ -114,14 +126,16 @@ end local function select_unit(cb) local choices = {} - for _,unit in ipairs(df.global.world.units.active) do + -- scan through units.all instead of units.active so we can choose starting dwarves on embark prep screen + for _,unit in ipairs(df.global.world.units.all) do + if not dfhack.units.isActive(unit) then goto continue end local target, sync_targets = get_unit_target(unit) - if target then - table.insert(choices, { - text=dfhack.units.getReadableName(unit), - data={target=target, sync_targets=sync_targets}, - }) - end + if not target then goto continue end + table.insert(choices, { + text=dfhack.units.getReadableName(unit), + data={target=target, sync_targets=sync_targets}, + }) + ::continue:: end dlg.showListPrompt('Rename', 'Select a unit to rename:', COLOR_WHITE, choices, function(_, choice) cb(choice.data.target, choice.data.sync_targets) end, @@ -129,7 +143,8 @@ local function select_unit(cb) end local function select_world(cb) - cb(df.global.world.world_data.name) + local target, sync_targets = get_world_target() + cb(target, sync_targets) end local function select_new_target(cb) @@ -148,7 +163,7 @@ local function select_new_target(cb) table.insert(choices, {text='A squad', data={fn=curry(select_squad, fort)}}) end end - if #df.global.world.units.active > 0 then + if #df.global.world.units.all > 0 then table.insert(choices, {text='A unit', data={fn=select_unit}}) end table.insert(choices, {text='This world', data={fn=select_world}}) @@ -514,14 +529,22 @@ end function Rename:clear_component_word(comp) self.target.words[comp] = -1 for _, sync_target in ipairs(self.sync_targets) do - sync_target.words[comp] = -1 + if type(sync_target) == 'function' then + sync_target() + else + sync_target.words[comp] = -1 + end end end function Rename:set_first_name(choice) self.target.first_name = translations[self.subviews.language:getOptionValue()].words[choice.data.idx].value for _, sync_target in ipairs(self.sync_targets) do - sync_target.first_name = self.target.first_name + if type(sync_target) == 'function' then + sync_target() + else + sync_target.first_name = self.target.first_name + end end end @@ -534,8 +557,12 @@ function Rename:set_component_word(_, choice) self.target.words[comp_choice.data.val] = choice.data.idx self.target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech for _, sync_target in ipairs(self.sync_targets) do - sync_target.words[comp_choice.data.val] = choice.data.idx - sync_target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech + if type(sync_target) == 'function' then + sync_target() + else + sync_target.words[comp_choice.data.val] = choice.data.idx + sync_target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech + end end end @@ -545,8 +572,12 @@ function Rename:set_language(val, prev_val) local idx = utils.linear_index(translations[prev_val].words, self.target.first_name, 'value') if idx then self.target.first_name = translations[val].words[idx].value end for _, sync_target in ipairs(self.sync_targets) do - sync_target.language = val - sync_target.first_name = self.target.first_name + if type(sync_target) == 'function' then + sync_target() + else + sync_target.language = val + sync_target.first_name = self.target.first_name + end end end @@ -591,8 +622,12 @@ function Rename:randomize_component_word(comp) self.target.words[comp] = words[idx] self.target.parts_of_speech[comp] = word_table.parts[comp][idx] for _, sync_target in ipairs(self.sync_targets) do - sync_target.words[comp] = words[idx] - sync_target.parts_of_speech[comp] = word_table.parts[comp][idx] + if type(sync_target) == 'function' then + sync_target() + else + sync_target.words[comp] = words[idx] + sync_target.parts_of_speech[comp] = word_table.parts[comp][idx] + end end end @@ -744,7 +779,29 @@ end -- Overlays -- -OVERLAY_WIDGETS = {} +WorldRenameOverlay = defclass(WorldRenameOverlay, overlay.OverlayWidget) +WorldRenameOverlay.ATTRS { + desc='Adds a button for renaming newly generated worlds.', + default_pos={x=57, y=3}, + default_enabled=true, + viewscreens='new_region', + frame={w=22, h=1}, +} + +function WorldRenameOverlay:init() + self:addviews{ + widgets.TextButton{ + frame={t=0, l=0}, + label='Rename world', + key='CUSTOM_CTRL_T', + on_activate=function() dfhack.run_script('gui/rename', '--world', '--no-target-selector') end, + }, + } +end + +OVERLAY_WIDGETS = { + world_rename=WorldRenameOverlay, +} -- -- CLI @@ -783,7 +840,7 @@ local function get_target(opts) target, sync_targets = get_unit_target(df.unit.find(opts.unit_id)) if not target then qerror('Unit not found') end elseif opts.world then - target = df.global.world.world_data.name + target, sync_targets = get_world_target() end return target, sync_targets end From 6aa777643e13d4868e223f875868f7626ae616e4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 02:01:28 -0800 Subject: [PATCH 276/811] only show overlay once world is loaded --- gui/rename.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/rename.lua b/gui/rename.lua index b18e0a13cc..ae5820aebb 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -786,6 +786,7 @@ WorldRenameOverlay.ATTRS { default_enabled=true, viewscreens='new_region', frame={w=22, h=1}, + visible=function() return dfhack.isWorldLoaded() end, } function WorldRenameOverlay:init() From 192c252efd8169d9ff3302593e4bc157b066b11b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 15:12:57 -0800 Subject: [PATCH 277/811] use word tables for randomization --- gui/rename.lua | 90 ++++++++++++++++++++++++++++---------------------- 1 file changed, 50 insertions(+), 40 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index ae5820aebb..a6d492ba35 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -511,13 +511,13 @@ function Rename:get_component_action_choices() table.insert(choices, {text='', data={fn=function() end}}) local randomize_text = {{text='[', pen=COLOR_RED}, 'Random', {text=']', pen=COLOR_RED}} - for val, comp in ipairs(df.language_name_component) do + for comp in ipairs(df.language_name_component) do local randomize_fn = self:callback('randomize_component_word', comp) table.insert(choices, {text=randomize_text, data={fn=randomize_fn}}) local clear_text = { - {text=function() return self.target.words[val] >= 0 and '[' or '' end, pen=COLOR_RED}, - {text=function() return self.target.words[val] >= 0 and 'Clear' or '' end }, - {text=function() return self.target.words[val] >= 0 and ']' or '' end, pen=COLOR_RED} + {text=function() return self.target.words[comp] >= 0 and '[' or '' end, pen=COLOR_RED}, + {text=function() return self.target.words[comp] >= 0 and 'Clear' or '' end }, + {text=function() return self.target.words[comp] >= 0 and ']' or '' end, pen=COLOR_RED} } local clear_fn = self:callback('clear_component_word', comp) table.insert(choices, {text=clear_text, data={fn=clear_fn}}) @@ -537,8 +537,9 @@ function Rename:clear_component_word(comp) end end -function Rename:set_first_name(choice) - self.target.first_name = translations[self.subviews.language:getOptionValue()].words[choice.data.idx].value +function Rename:set_first_name(word_idx) + self.target.first_name = translations[self.subviews.language:getOptionValue()].words[word_idx].value + self.target.has_name = true -- support giving names to previously unnamed units for _, sync_target in ipairs(self.sync_targets) do if type(sync_target) == 'function' then sync_target() @@ -548,24 +549,28 @@ function Rename:set_first_name(choice) end end -function Rename:set_component_word(_, choice) - local _, comp_choice = self.subviews.component_list:getSelected() - if comp_choice.data.is_first_name then - self:set_first_name(choice) - return - end - self.target.words[comp_choice.data.val] = choice.data.idx - self.target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech +function Rename:set_component_word_by_data(component, word_idx, part_of_speech) + self.target.words[component] = word_idx + self.target.parts_of_speech[component] = part_of_speech for _, sync_target in ipairs(self.sync_targets) do if type(sync_target) == 'function' then sync_target() else - sync_target.words[comp_choice.data.val] = choice.data.idx - sync_target.parts_of_speech[comp_choice.data.val] = choice.data.part_of_speech + sync_target.words[component] = word_idx + sync_target.parts_of_speech[component] = part_of_speech end end end +function Rename:set_component_word(_, choice) + local _, comp_choice = self.subviews.component_list:getSelected() + if comp_choice.data.is_first_name then + self:set_first_name(choice.data.idx) + return + end + self:set_component_word_by_data(comp_choice.data.val, choice.data.idx, choice.data.part_of_speech) +end + function Rename:set_language(val, prev_val) self.target.language = val -- translate current first name into target language @@ -581,7 +586,7 @@ function Rename:set_language(val, prev_val) end end -local langauge_name_type_to_category = { +local language_name_type_to_category = { [df.language_name_type.Figure] = {df.language_name_category.Unit}, [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, [df.language_name_type.Civilization] = {df.language_name_category.EntityMerchantCompany}, @@ -590,7 +595,7 @@ local langauge_name_type_to_category = { [df.language_name_type.World] = {df.language_name_category.Region}, [df.language_name_type.EntitySite] = {df.language_name_category.Keep}, [df.language_name_type.Temple] = {df.language_name_category.Temple}, - [df.language_name_type.MeadHall] = {df.language_name_category.MeadHall}, + [df.language_name_type.FoodStore] = {df.language_name_category.MeadHall}, [df.language_name_type.Library] = {df.language_name_category.Library}, [df.language_name_type.Guildhall] = {df.language_name_category.Guildhall}, [df.language_name_type.Hospital] = {df.language_name_category.Hospital}, @@ -599,40 +604,45 @@ local langauge_name_type_to_category = { local language_name_component_to_word_table_index = { [df.language_name_component.FrontCompound] = df.language_word_table_index.FrontCompound, [df.language_name_component.RearCompound] = df.language_word_table_index.RearCompound, - [df.language_name_component.FrontCompound] = df.language_word_table_index.FirstName, [df.language_name_component.FirstAdjective] = df.language_word_table_index.Adjectives, [df.language_name_component.SecondAdjective] = df.language_word_table_index.Adjectives, - [df.language_name_component.FrontCompound] = df.language_word_table_index.TheX, - [df.language_name_component.FrontCompound] = df.language_word_table_index.OfX, - + [df.language_name_component.HyphenCompound] = df.language_word_table_index.FrontCompound, + [df.language_name_component.TheX] = df.language_word_table_index.TheX, + [df.language_name_component.OfX] = df.language_word_table_index.OfX, } +local function get_random_word(category, word_table_index) + local word_table = language.word_table[0][category] + local words = word_table.words[word_table_index] + local idx = #words > 0 and math.random(#words)-1 or -1 + local word = idx >= 0 and words[idx] or -1 + local part_of_speech = idx >= 0 and word_table.parts[word_table_index][idx] or df.part_of_speech.Noun + return word, part_of_speech +end + function Rename:randomize_first_name() if self.target.type ~= df.language_name_type.Figure then return end - local choices = self:get_word_choices(df.language_name_component.TheX) - self:set_first_name(choices[math.random(#choices)]) + local word_idx = get_random_word(df.language_name_category.Unit, df.language_word_table_index.FirstName) + self:set_first_name(word_idx) end function Rename:randomize_component_word(comp) - local categories = langauge_name_type_to_category[self.target.type] - local category = categories[math.random(#categories)] - local word_table = language.word_table[0][category] - local words = word_table.words[comp] - local idx = math.random(#words)-1 - self.target.words[comp] = words[idx] - self.target.parts_of_speech[comp] = word_table.parts[comp][idx] - for _, sync_target in ipairs(self.sync_targets) do - if type(sync_target) == 'function' then - sync_target() - else - sync_target.words[comp] = words[idx] - sync_target.parts_of_speech[comp] = word_table.parts[comp][idx] - end - end + local categories = language_name_type_to_category[self.target.type] + local category = categories and categories[math.random(#categories)] or df.language_name_category.MeadHall + local word_idx, part_of_speech = get_random_word(category, language_name_component_to_word_table_index[comp]) + self:set_component_word_by_data(comp, word_idx, part_of_speech) end function Rename:generate_random_name() - print('TODO: generate_random_name') + print('TODO: call dfhack.GenerateName API once it exists') + -- dfhack.GenerateName(self.target) + -- for _, sync_target in ipairs(self.sync_targets) do + -- if type(sync_target) == 'function' then + -- sync_target() + -- else + -- df.assign(sync_target, self.target) + -- end + -- end end local part_of_speech_to_display = { From b342b43c86190f7d141d1fcd50f03dcd7a6ee8dd Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 15:23:09 -0800 Subject: [PATCH 278/811] delegate word table lookups to upcoming library API --- gui/rename.lua | 59 ++++++++++---------------------------------------- 1 file changed, 12 insertions(+), 47 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index a6d492ba35..7859811443 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -286,13 +286,13 @@ function Rename:init(info) end, visible=info.show_selector, }, - widgets.HotkeyLabel{ - frame={t=0, r=0}, - key='CUSTOM_CTRL_G', - label='Generate random name', - auto_width=true, - on_activate=self:callback('generate_random_name'), - }, + -- widgets.HotkeyLabel{ + -- frame={t=0, r=0}, + -- key='CUSTOM_CTRL_G', + -- label='Generate random name', + -- auto_width=true, + -- on_activate=self:callback('generate_random_name'), + -- }, widgets.Label{ frame={t=2}, text={{pen=COLOR_YELLOW, text=function() return pad_text(dfhack.TranslateName(self.target), self.frame_body.width) end}}, @@ -586,51 +586,16 @@ function Rename:set_language(val, prev_val) end end -local language_name_type_to_category = { - [df.language_name_type.Figure] = {df.language_name_category.Unit}, - [df.language_name_type.Artifact] = {df.language_name_category.Artifact, df.language_name_category.ArtifactEvil}, - [df.language_name_type.Civilization] = {df.language_name_category.EntityMerchantCompany}, - [df.language_name_type.Squad] = {df.language_name_category.Battle}, - [df.language_name_type.Site] = {df.language_name_category.Keep}, - [df.language_name_type.World] = {df.language_name_category.Region}, - [df.language_name_type.EntitySite] = {df.language_name_category.Keep}, - [df.language_name_type.Temple] = {df.language_name_category.Temple}, - [df.language_name_type.FoodStore] = {df.language_name_category.MeadHall}, - [df.language_name_type.Library] = {df.language_name_category.Library}, - [df.language_name_type.Guildhall] = {df.language_name_category.Guildhall}, - [df.language_name_type.Hospital] = {df.language_name_category.Hospital}, -} - -local language_name_component_to_word_table_index = { - [df.language_name_component.FrontCompound] = df.language_word_table_index.FrontCompound, - [df.language_name_component.RearCompound] = df.language_word_table_index.RearCompound, - [df.language_name_component.FirstAdjective] = df.language_word_table_index.Adjectives, - [df.language_name_component.SecondAdjective] = df.language_word_table_index.Adjectives, - [df.language_name_component.HyphenCompound] = df.language_word_table_index.FrontCompound, - [df.language_name_component.TheX] = df.language_word_table_index.TheX, - [df.language_name_component.OfX] = df.language_word_table_index.OfX, -} - -local function get_random_word(category, word_table_index) - local word_table = language.word_table[0][category] - local words = word_table.words[word_table_index] - local idx = #words > 0 and math.random(#words)-1 or -1 - local word = idx >= 0 and words[idx] or -1 - local part_of_speech = idx >= 0 and word_table.parts[word_table_index][idx] or df.part_of_speech.Noun - return word, part_of_speech -end - function Rename:randomize_first_name() if self.target.type ~= df.language_name_type.Figure then return end - local word_idx = get_random_word(df.language_name_category.Unit, df.language_word_table_index.FirstName) - self:set_first_name(word_idx) + local choices = self:get_word_choices(df.language_name_component.TheX) + self:set_first_name(choices[math.random(#choices)].data.idx) end function Rename:randomize_component_word(comp) - local categories = language_name_type_to_category[self.target.type] - local category = categories and categories[math.random(#categories)] or df.language_name_category.MeadHall - local word_idx, part_of_speech = get_random_word(category, language_name_component_to_word_table_index[comp]) - self:set_component_word_by_data(comp, word_idx, part_of_speech) + local choices = self:get_word_choices(df.language_name_component.TheX) + local choice = choices[math.random(#choices)] + self:set_component_word_by_data(comp, choice.data.idx, choice.data.part_of_speech) end function Rename:generate_random_name() From 08bffc8aec26116a453273d03b1eec7e2cdbd6fa Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 15:39:25 -0800 Subject: [PATCH 279/811] update docs and clean up code --- docs/gui/rename.rst | 39 ++++++++++++++++++++++++++------------- gui/rename.lua | 12 +++++++++--- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index d72255d0ac..c643186455 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -2,18 +2,22 @@ gui/rename ========== .. dfhack-tool:: - :summary: Modify the name of anything that is nameable. + :summary: Edit in-game language-based names. :tags: adventure fort productivity animals items units -Once you select a target (by clicking on the game map, by passing a commandline -parameter, or by using the provided selection widget) this tool allows you -change its language name, generate a new random name, or rename it with your -preferred component words. It provides an interface similar to the in-game -naming panel that you can use to customize your fortress name at embark. That -is, it allows you to choose words from an in-game language to assemble a name, -just like the default names that the game generates. You will be able to assign -units new given and last names. You can also use this tool to set freeform -"nicknames" for targets that support it. +Once you select a target (by clicking on something on the game map, by passing +a commandline parameter, or by using the selection dialog) this tool allows you +change the language of the name, generate a new random name, or replace +components of the name with your preferred words. + +`gui/rename` provides an interface similar to the in-game naming panel that you +can use to customize your fortress name at embark. That is, it allows you to +choose words from an in-game language to assemble a name, just like the default +names that the game generates. You will be able to assign units new given and +last names, or even rename the world itself. + +You can run `gui/rename` while on the "prepare carefully" embark screen to +rename your starting dwarves. Usage ----- @@ -22,6 +26,16 @@ Usage gui/rename [] +The selection dialog will appear if no options are provided. You can +interactively choose one of the following to rename: + +- An artifact on the current map +- A location (e.g. tavern, hospital, guildhall, temple) on the current map +- The current fortress (or adventurer site) +- A squad belonging to the current fortress +- A unit on the current map +- The world + Examples -------- @@ -38,6 +52,8 @@ Examples Options ------- +Targets specified via these options do not need to be on the local map. + ``-a``, ``--artifact `` Rename the artifact with the given item ID. ``-e``, ``--entity `` @@ -68,8 +84,5 @@ Overlays This tool supports the following overlays: -``gui/rename.embark`` - Adds widgets to the embark preparation screen for renaming the starting - dwarves. ``gui/rename.world`` Adds a widget to the world generation screen for renaming the world. diff --git a/gui/rename.lua b/gui/rename.lua index 7859811443..5908856eff 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -267,6 +267,10 @@ function Rename:init(info) self.sync_targets = info.sync_targets or {} self.cache = {} + if self.target.type == df.language_name_type.NONE then + self.target.type = df.language_name_type.Figure + end + local language_options, max_lang_name_width = get_language_options() self:addviews{ @@ -538,8 +542,10 @@ function Rename:clear_component_word(comp) end function Rename:set_first_name(word_idx) + -- support giving names to previously unnamed units + self.target.has_name = true + self.target.first_name = translations[self.subviews.language:getOptionValue()].words[word_idx].value - self.target.has_name = true -- support giving names to previously unnamed units for _, sync_target in ipairs(self.sync_targets) do if type(sync_target) == 'function' then sync_target() @@ -751,7 +757,7 @@ function RenameScreen:onDismiss() end -- --- Overlays +-- WorldRenameOverlay -- WorldRenameOverlay = defclass(WorldRenameOverlay, overlay.OverlayWidget) @@ -776,7 +782,7 @@ function WorldRenameOverlay:init() end OVERLAY_WIDGETS = { - world_rename=WorldRenameOverlay, + world=WorldRenameOverlay, } -- From e6c1be6f12db57d5d136db63e16ccc32ae3f92de Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 15:41:47 -0800 Subject: [PATCH 280/811] update changelog --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 5b8aa1c065..c93c9ed114 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,10 +28,12 @@ Template for new versions: ## New Tools - `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map +- `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (e.g. units, governments, fortresses, or the world) ## New Features - `gui/settings-manager`: new overlay on the Labor -> Standing Orders tab for configuring the number of barrels to reserve for job use (so you can brew alcohol and not have all your barrels claimed by stockpiles for container storage) - `gui/settings-manager`: standing orders save/load now includes the reserved barrels setting +- `gui/rename`: add overlay to worldgen screen allowing you to rename the world before the new world is saved ## Fixes - `fix/dry-buckets`: don't empty buckets for wells that are actively in use From 02ef7ffa598a7653ec9f588f33b93f9c24e5ea22 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 2 Jan 2025 16:06:38 -0800 Subject: [PATCH 281/811] more docs --- docs/gui/rename.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index c643186455..bf58025a1f 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -19,6 +19,15 @@ last names, or even rename the world itself. You can run `gui/rename` while on the "prepare carefully" embark screen to rename your starting dwarves. +Start typing to search for a word. You can search in English, in the selected +native language, or by the part of speech. Click on a word to assign it to the +selected name component slot. You can also clear or randomize each individual +name component slot. + +When giving a name to a unit that didn't previously have a name, you must +assign a word to the First Name slot. Otherwise, the game will not display the +name for the unit. + Usage ----- From 1b944df3dccf94449ef7bdd0de809041100a5335 Mon Sep 17 00:00:00 2001 From: Myk Date: Thu, 2 Jan 2025 18:29:09 -0800 Subject: [PATCH 282/811] Ensure the name type is set when a new target is loaded --- gui/rename.lua | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 5908856eff..93eca5900b 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -267,9 +267,12 @@ function Rename:init(info) self.sync_targets = info.sync_targets or {} self.cache = {} - if self.target.type == df.language_name_type.NONE then - self.target.type = df.language_name_type.Figure + local function normalize_name() + if self.target.type == df.language_name_type.NONE then + self.target.type = df.language_name_type.Figure + end end + normalize_name() local language_options, max_lang_name_width = get_language_options() @@ -285,6 +288,7 @@ function Rename:init(info) select_new_target(function(target, sync_targets) if not target then return end self.target, self.sync_targets = target, sync_targets or {} + normalize_name() self.subviews.language:setOption(self.target.language) end) end, From 55a2429440e7676260a888679944ce8f05d2059d Mon Sep 17 00:00:00 2001 From: Myk Date: Thu, 2 Jan 2025 18:40:44 -0800 Subject: [PATCH 283/811] Ensure first name is not selected when changing targets to a non unit --- gui/rename.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/rename.lua b/gui/rename.lua index 93eca5900b..9a3fceb91f 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -290,6 +290,7 @@ function Rename:init(info) self.target, self.sync_targets = target, sync_targets or {} normalize_name() self.subviews.language:setOption(self.target.language) + self:refresh_list() end) end, visible=info.show_selector, From a88b27da13d57ad2062dbca6ae3ff18984ec411e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 3 Jan 2025 03:34:27 -0800 Subject: [PATCH 284/811] fix up errors found by testing --- gui/rename.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 9a3fceb91f..1bf2658601 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -341,7 +341,7 @@ function Rename:init(info) widgets.List{ frame={t=2, l=0, b=4, w=ENGLISH_COL_WIDTH+2}, view_id='component_list', - on_select=self:callback('refresh_list'), + on_select=function() self:refresh_list() end, choices=self:get_component_choices(), row_height=3, scroll_keys={}, @@ -715,7 +715,7 @@ function Rename:refresh_list(sort_widget, sort_fn) local clist = self.subviews.component_list if not clist then return end if self.target.type ~= df.language_name_type.Figure and clist:getSelected() == 1 then - clist:setSelected(self.prev_selected_component or 2) + clist:setSelected(self.prev_selected_component ~= 1 and self.prev_selected_component or 2) end self.prev_selected_component = clist:getSelected() @@ -733,7 +733,7 @@ function Rename:refresh_list(sort_widget, sort_fn) list:setFilter('') local _, comp_choice = clist:getSelected() local choices = self:get_word_choices(comp_choice.data.val) - table.sort(choices, self.subviews.sort:getOptionValue()) + table.sort(choices, sort_fn) list:setChoices(choices) list:setFilter(saved_filter) end From a10ce265c4086d1e01b6f15cecf086b9afe7ef66 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 3 Jan 2025 03:37:40 -0800 Subject: [PATCH 285/811] don't use keybindings that are only available in the beta --- gui/rename.lua | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 1bf2658601..73d5ee74be 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -355,7 +355,8 @@ function Rename:init(info) }, widgets.HotkeyLabel{ frame={b=3, l=0}, - key='SECONDSCROLL_UP', + --key='SECONDSCROLL_UP', -- use when this is available in mainline DF + key='STRING_A045', label='Prev component', on_activate=function() local clist = self.subviews.component_list @@ -366,7 +367,8 @@ function Rename:init(info) }, widgets.HotkeyLabel{ frame={b=2, l=0}, - key='SECONDSCROLL_DOWN', + -- key='SECONDSCROLL_DOWN', -- use when this is available in mainline DF + key='STRING_A043', label='Next component', on_activate=function() local clist = self.subviews.component_list @@ -426,7 +428,8 @@ function Rename:init(info) view_id='search', frame={t=0, l=22}, label_text='Search: ', - ignore_keys={'SECONDSCROLL_DOWN', 'SECONDSCROLL_UP'} + -- ignore_keys={'SECONDSCROLL_DOWN', 'SECONDSCROLL_UP'} + ignore_keys={'STRING_A043', 'STRING_A045'}, }, widgets.CycleHotkeyLabel{ view_id='sort_english', From 1bb317331c32a6d2609cc8b91b90680ee362b729 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 3 Jan 2025 05:26:05 -0800 Subject: [PATCH 286/811] more gui/rename docs --- docs/gui/rename.rst | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index bf58025a1f..1922b6e4cd 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -28,6 +28,9 @@ When giving a name to a unit that didn't previously have a name, you must assign a word to the First Name slot. Otherwise, the game will not display the name for the unit. +When you change the language of a unit's name, the unit's existing first name +will be translated into the new language. + Usage ----- From 1ed5e22c9f8340dc09f01ff2552b094faa103593 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 4 Jan 2025 16:51:39 +0100 Subject: [PATCH 287/811] Create tooltips.lua --- gui/tooltips.lua | 281 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 281 insertions(+) create mode 100644 gui/tooltips.lua diff --git a/gui/tooltips.lua b/gui/tooltips.lua new file mode 100644 index 0000000000..b3b5212d88 --- /dev/null +++ b/gui/tooltips.lua @@ -0,0 +1,281 @@ +-- Show tooltips on units and/or mouse + +local RELOAD = false -- set to true when actively working on this script + +local gui = require('gui') +local widgets = require('gui.widgets') +local ResizingPanel = require('gui.widgets.containers.resizing_panel') + +-------------------------------------------------------------------------------- + +local follow_units = true; +local follow_mouse = true; +local function change_follow_units(new, old) + follow_units = new +end +local function change_follow_mouse(new, old) + follow_mouse = new +end + +local shortenings = { + ["Store item in stockpile"] = "Store item", +} + +-------------------------------------------------------------------------------- + +local TITLE = "Tooltips" + +if RELOAD then TooltipControlWindow = nil end +TooltipControlWindow = defclass(TooltipControlWindow, widgets.Window) +TooltipControlWindow.ATTRS { + frame_title=TITLE, + frame_inset=0, + resizable=false, + frame = { + w = 25, + h = 4, + -- just under the minimap: + r = 2, + t = 18, + }, +} + +function TooltipControlWindow:init() + self:addviews{ + widgets.ToggleHotkeyLabel{ + view_id = 'btn_follow_units', + frame={t=0, h=1}, + label="Follow units", + key='CUSTOM_ALT_U', + on_change=change_follow_units, + }, + widgets.ToggleHotkeyLabel{ + view_id = 'btn_follow_mouse', + frame={t=1, h=1}, + label="Follow mouse", + key='CUSTOM_ALT_M', + on_change=change_follow_mouse, + }, + } +end + +local function GetUnitJob(unit) + local job = unit.job + if job and job.current_job then + return dfhack.job.getName(job.current_job) + end + return nil +end + +local function GetUnitNameAndJob(unit) + local sb = {} + sb[#sb+1] = dfhack.units.getReadableName(unit) + local jobName = GetUnitJob(unit) + if jobName then + sb[#sb+1] = ": " + sb[#sb+1] = jobName + end + return table.concat(sb) +end + +local function GetTooltipText(x,y,z) + local txt = {} + local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z) or {} -- todo: maybe (optionally) use filter parameter here? + + for _,unit in pairs(units) do + txt[#txt+1] = GetUnitNameAndJob(unit) + txt[#txt+1] = NEWLINE + end + + return txt +end + +-------------------------------------------------------------------------------- +-- MouseTooltip is an almost copy&paste of the DimensionsTooltip +-- +if RELOAD then MouseTooltip = nil end +MouseTooltip = defclass(MouseTooltip, ResizingPanel) + +MouseTooltip.ATTRS{ + frame_style=gui.FRAME_THIN, + frame_background=gui.CLEAR_PEN, + no_force_pause_badge=true, + auto_width=true, + display_offset={x=3, y=3}, +} + +function MouseTooltip:init() + ensure_key(self, 'frame').w = 17 + self.frame.h = 4 + + self.label = widgets.Label{ + frame={t=0}, + auto_width=true, + } + + self:addviews{ + widgets.Panel{ + -- set minimum size for tooltip frame so the DFHack frame badge fits + frame={t=0, l=0, w=7, h=2}, + }, + self.label, + } +end + +function MouseTooltip:render(dc) + if not follow_mouse then return end + + local x, y = dfhack.screen.getMousePos() + if not x then return end + + local pos = dfhack.gui.getMousePos() + local text = GetTooltipText(pos2xyz(pos)) + if #text == 0 then return end + self.label:setText(text) + + local sw, sh = dfhack.screen.getWindowSize() + local frame_width = math.max(9, self.label:getTextWidth() + 2) + self.frame.l = math.min(x + self.display_offset.x, sw - frame_width) + self.frame.t = math.min(y + self.display_offset.y, sh - self.frame.h) + self:updateLayout() + MouseTooltip.super.render(self, dc) +end + +-------------------------------------------------------------------------------- + +if RELOAD then TooltipsVizualizer = nil end +TooltipsVizualizer = defclass(TooltipsVizualizer, gui.ZScreen) +TooltipsVizualizer.ATTRS{ + focus_path='TooltipsVizualizer', + pass_movement_keys=true, +} + +function TooltipsVizualizer:init() + local controls = TooltipControlWindow{view_id = 'controls'} + local tooltip = MouseTooltip{view_id = 'tooltip'} + self:addviews{controls, tooltip} +end + +-- map coordinates -> interface layer coordinates +function GetScreenCoordinates(map_coord) + if not map_coord then return end + + -- -> map viewport offset + local vp = df.global.world.viewport + local vp_Coord = vp.window_x -- is actually coord + local map_offset_by_vp = { + x = map_coord.x - vp_Coord.x, + y = map_coord.y - vp_Coord.y, + z = map_coord.z - vp_Coord.z, + } + -- -> pixel offset + local gps = df.global.gps + local map_tile_pixels = gps.viewport_zoom_factor // 4; + local screen_coord_px = { + x = map_tile_pixels * map_offset_by_vp.x, + y = map_tile_pixels * map_offset_by_vp.y, + } + -- -> interface layer coordinates + local screen_coord_text = { + x = math.ceil( screen_coord_px.x / gps.tile_pixel_x ), + y = math.ceil( screen_coord_px.y / gps.tile_pixel_y ), + } + + return screen_coord_text +end + +function TooltipsVizualizer:onRenderFrame(dc, rect) + TooltipsVizualizer.super.onRenderFrame(self, dc, rect) + + if not follow_units then return end + + if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then + return + end + + local vp = df.global.world.viewport + local topleft = vp.window_x + local width = vp.max_x + local height = vp.max_y + local bottomright = {x = topleft.x + width, y = topleft.y + height, z = topleft.z} + + local units = dfhack.units.getUnitsInBox(topleft.x,topleft.y,topleft.z,bottomright.x,bottomright.y,bottomright.z) or {} + if #units == 0 then return end + + local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) + local pen = COLOR_WHITE + + local used_tiles = {} + for i = #units, 1, -1 do + local unit = units[i] + local txt = GetUnitJob(unit) + if not txt then goto continue end + + txt = shortenings[txt] or txt + + local pos = xyz2pos(dfhack.units.getPosition(unit)) + if not pos then goto continue end + + local scrPos = GetScreenCoordinates(pos) + local y = scrPos.y - 1 -- subtract 1 to move the text over the heads + local x = scrPos.x + oneTileOffset.x - 1 -- subtract 1 to move the text inside the map tile + + -- to resolve overlaps, we'll mark every coordinate we write anything in, + -- and then check if the new tooltip will overwrite any used coordinate. + -- if it will, try the next row, to a maximum offset of 4. + local row + local dy = 0 + -- todo: search for the "best" offset instead, f.e. max `usedAt` value, with `-1` the best + local usedAt = -1 + for yOffset = 0, 4 do + dy = yOffset + + row = used_tiles[y + dy] + if not row then + row = {} + used_tiles[y + dy] = row + end + + usedAt = -1 + for j = 0, #txt - 1 do + if row[x + j] then + usedAt = j + break + end + end + + if usedAt == -1 then break end + end -- for dy + + -- in case there isn't enough space, cut the text off + if usedAt > 0 then + txt = txt:sub(0, usedAt - 1) .. '_' + end + + dc:seek(x, y + dy):pen(pen):string(txt) + + -- mark coordinates as used + for j = 0, #txt - 1 do + row[x + j] = true + end + + ::continue:: + end +end + +function TooltipsVizualizer:onDismiss() + view = nil +end + +---------------------------------------------------------------- + +if not dfhack.isMapLoaded() then + qerror('gui/tooltips requires a map to be loaded') +end + +if RELOAD and view then + view:dismiss() + -- view is nil now +end + +view = view and view:raise() or TooltipsVizualizer{}:show() From 7abc35586e913e8748679b25abf063cefd72337f Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 4 Jan 2025 17:36:39 +0100 Subject: [PATCH 288/811] Create tooltips.rst --- docs/gui/tooltips.rst | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 docs/gui/tooltips.rst diff --git a/docs/gui/tooltips.rst b/docs/gui/tooltips.rst new file mode 100644 index 0000000000..290196560b --- /dev/null +++ b/docs/gui/tooltips.rst @@ -0,0 +1,15 @@ +gui/tooltips +============ + +.. dfhack-tool:: + :summary: Show tooltips with useful info. + :tags: fort inspection + +This script shows "tooltips" following units and/or mouse with job names. + +Usage +----- + +:: + + gui/tooltips From 802169cac7a61998def701ac3514f5ee59b0807d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 5 Jan 2025 09:06:25 -0800 Subject: [PATCH 289/811] add govt and civ to rename options in fort mode --- gui/rename.lua | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 73d5ee74be..00ed9b0369 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -105,7 +105,7 @@ local function select_location(site, cb) choices, function(_, choice) cb(choice.data.target) end, nil, nil, true) end -local function select_site(site, cb) +local function select_entity(site, cb) cb(site.name) end @@ -153,16 +153,24 @@ local function select_new_target(cb) table.insert(choices, {text='An artifact', data={fn=select_artifact}}) end local site = dfhack.world.getCurrentSite() + local is_fort_mode = dfhack.world.isFortressMode() + local fort = is_fort_mode and df.historical_entity.find(df.global.plotinfo.group_id) + local civ = is_fort_mode and df.historical_entity.find(df.global.plotinfo.civ_id) if site then if #site.buildings > 0 then table.insert(choices, {text='A location', data={fn=curry(select_location, site)}}) end - table.insert(choices, {text='This fortress', data={fn=curry(select_site, site)}}) - local fort = df.historical_entity.find(df.global.plotinfo.group_id) + table.insert(choices, {text='This fortress/site', data={fn=curry(select_entity, site)}}) if fort and #fort.squads > 0 then table.insert(choices, {text='A squad', data={fn=curry(select_squad, fort)}}) end end + if fort then + table.insert(choices, {text='The government of this fortress', data={fn=curry(select_entity, fort)}}) + end + if civ then + table.insert(choices, {text='The civilization of this fortress', data={fn=curry(select_entity, civ)}}) + end if #df.global.world.units.all > 0 then table.insert(choices, {text='A unit', data={fn=select_unit}}) end From 9dbfc4d7d85413aa89d738fe344bcf11b14044c2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 5 Jan 2025 17:10:52 -0800 Subject: [PATCH 290/811] more gui/rename docs --- docs/gui/rename.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index 1922b6e4cd..39b64c632a 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -45,9 +45,16 @@ interactively choose one of the following to rename: - A location (e.g. tavern, hospital, guildhall, temple) on the current map - The current fortress (or adventurer site) - A squad belonging to the current fortress +- The civilization of the current fortress +- The government of the current fortress - A unit on the current map - The world +Some of the options above will not show up if there is nothing of the given +type to rename (e.g. you won't get the option to rename a squad if there are no +squads, and you won't get an option to rename the government of the fortress if +you aren't in fortress mode). + Examples -------- From 693628c8ff68eeb7831a65c166c3244363b7d4a3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 5 Jan 2025 18:27:01 -0800 Subject: [PATCH 291/811] fix wood detection logic for trading --- changelog.txt | 1 + internal/caravan/common.lua | 74 ++++++++++++++++++++++++++++++------- 2 files changed, 61 insertions(+), 14 deletions(-) diff --git a/changelog.txt b/changelog.txt index c93c9ed114..7d91af061e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -38,6 +38,7 @@ Template for new versions: ## Fixes - `fix/dry-buckets`: don't empty buckets for wells that are actively in use - `gui/unit-info-viewer`: skill progress bars now show correct XP thresholds for skills past Legendary+5 +- `caravan`: no longer incorrectly identify wood-based plant items and plant-based soaps as being ethically unsuitable for trading with the elves ## Misc Improvements - `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 135762376c..061accb5a5 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -2,6 +2,7 @@ local dialogs = require('gui.dialogs') local predicates = reqscript('internal/caravan/predicates') +local utils = require('utils') local widgets = require('gui.widgets') CH_UP = string.char(30) @@ -625,35 +626,80 @@ function scan_banned(item, risky_items) return false, false end -local function is_wood_based(mat_type, mat_index) - if mat_type == df.builtin_mats.LYE or - mat_type == df.builtin_mats.GLASS_CLEAR or - mat_type == df.builtin_mats.GLASS_CRYSTAL or - (mat_type == df.builtin_mats.COAL and mat_index == 1) or - mat_type == df.builtin_mats.POTASH or - mat_type == df.builtin_mats.ASH or - mat_type == df.builtin_mats.PEARLASH - then +local function is_wood_based_material(mat_type, mat_index) + if mat_type == df.builtin_mats.GLASS_CLEAR or mat_type == df.builtin_mats.GLASS_CRYSTAL then return true end local mi = dfhack.matinfo.decode(mat_type, mat_index) - return mi and mi.material and + return mi and mi.mode == 'plant' and mi.material and (mi.material.flags.WOOD or - mi.material.flags.STRUCTURAL_PLANT_MAT or - mi.material.flags.SOAP) + mi.material.flags.STRUCTURAL_PLANT_MAT) +end + +local item_types_never_wood = utils.invert{ + df.item_type.SMALLGEM, + df.item_type.BLOCKS, + df.item_type.ROUGH, + df.item_type.BOULDER, + df.item_type.CORPSE, + df.item_type.CORPSEPIECE, + df.item_type.REMAINS, + df.item_type.MEAT, + df.item_type.FISH, + df.item_type.FISH_RAW, + df.item_type.VERMIN, + df.item_type.PET, + df.item_type.SEEDS, + df.item_type.PLANT, + df.item_type.SKIN_TANNED, + df.item_type.PLANT_GROWTH, + df.item_type.DRINK, + df.item_type.CHEESE, + df.item_type.FOOD, + df.item_type.COIN, + df.item_type.GLOB, + df.item_type.ROCK, + df.item_type.EGG, +} + +local function is_wood_based_item(item) + local itype = item:getType() + + if item_types_never_wood[itype] then return false end + + local mat_type, mat_index = item:getMaterial(), item:getMaterialIndex() + + if itype == df.item_type.BAR then + if mat_type == df.builtin_mats.POTASH or + mat_type == df.builtin_mats.ASH or + mat_type == df.builtin_mats.PEARLASH or + (mat_type == df.builtin_mats.COAL and mat_index == 1) + then + return true + end + local mi = dfhack.matinfo.decode(mat_type, mat_index) + return mi and mi.mode == 'creature' + elseif itype == df.item_type.LIQUID_MISC then + return mat_type == df.builtin_mats.LYE + elseif itype == df.item_type.WEAPON then + local mi = dfhack.matinfo.decode(mat_type, mat_index) + return mi and mi.mode == 'inorganic' and mi.material and not mi.material.flags.IS_METAL + end + + return is_wood_based_material(mat_type, mat_index) end function has_wood(item) if item.flags2.grown then return false end - if is_wood_based(item:getMaterial(), item:getMaterialIndex()) then + if is_wood_based_item(item) then return true end if item:hasImprovements() then for _, imp in ipairs(item.improvements) do - if is_wood_based(imp.mat_type, imp.mat_index) then + if is_wood_based_material(imp.mat_type, imp.mat_index) then return true end end From 66dce0d4f116b5e3ca560ea927592dd765f19a44 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Mon, 6 Jan 2025 11:16:22 +0100 Subject: [PATCH 292/811] Update docs/gui/tooltips.rst Co-authored-by: Myk --- docs/gui/tooltips.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/tooltips.rst b/docs/gui/tooltips.rst index 290196560b..673fc1087b 100644 --- a/docs/gui/tooltips.rst +++ b/docs/gui/tooltips.rst @@ -2,7 +2,7 @@ gui/tooltips ============ .. dfhack-tool:: - :summary: Show tooltips with useful info. + :summary: Show name and job tooltips near units on map. :tags: fort inspection This script shows "tooltips" following units and/or mouse with job names. From c27777a6f7aa360087be33cca4f932bb560b4181 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Mon, 6 Jan 2025 11:17:00 +0100 Subject: [PATCH 293/811] Update gui/tooltips.lua Co-authored-by: Myk --- gui/tooltips.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index b3b5212d88..5ebf557831 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -82,7 +82,7 @@ local function GetTooltipText(x,y,z) local txt = {} local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z) or {} -- todo: maybe (optionally) use filter parameter here? - for _,unit in pairs(units) do + for _,unit in ipairs(units) do txt[#txt+1] = GetUnitNameAndJob(unit) txt[#txt+1] = NEWLINE end From 0ca5a9ab825346800905bdb566c24d61b648ceac Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Tue, 7 Jan 2025 23:27:48 +0100 Subject: [PATCH 294/811] enter emoticons --- gui/tooltips.lua | 28 +++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index 5ebf557831..b54dc30481 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -59,6 +59,16 @@ function TooltipControlWindow:init() } end +local function GetUnitHappiness(unit) + -- keep in mind, this will look differently with game's font + local mapToEmoticon = {[0] = "=C", ":C", ":(", ":]", ":)", ":D", "=D" } + -- same as in ASCII mode, but for then middle (3), which is GREY instead of WHITE + local mapToColor = {[0] = COLOR_RED, COLOR_LIGHTRED, COLOR_YELLOW, COLOR_GREY, COLOR_GREEN, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN} + local stressCat = dfhack.units.getStressCategory(unit) + if stressCat > 6 then stressCat = 6 end + return mapToEmoticon[stressCat], mapToColor[stressCat] +end + local function GetUnitJob(unit) local job = unit.job if job and job.current_job then @@ -208,14 +218,17 @@ function TooltipsVizualizer:onRenderFrame(dc, rect) local used_tiles = {} for i = #units, 1, -1 do local unit = units[i] - local txt = GetUnitJob(unit) - if not txt then goto continue end - txt = shortenings[txt] or txt + local happiness, happyPen = GetUnitHappiness(unit) + local job = GetUnitJob(unit) + job = shortenings[job] or job + if not job and not happiness then goto continue end local pos = xyz2pos(dfhack.units.getPosition(unit)) if not pos then goto continue end + local txt = table.concat({happiness, job}, " ") + local scrPos = GetScreenCoordinates(pos) local y = scrPos.y - 1 -- subtract 1 to move the text over the heads local x = scrPos.x + oneTileOffset.x - 1 -- subtract 1 to move the text inside the map tile @@ -249,10 +262,15 @@ function TooltipsVizualizer:onRenderFrame(dc, rect) -- in case there isn't enough space, cut the text off if usedAt > 0 then - txt = txt:sub(0, usedAt - 1) .. '_' + local s = happiness and #happiness + 1 or 0 + job = job:sub(0, usedAt - s - 1) .. '_' + txt = txt:sub(0, usedAt - 1) .. '_' -- for marking end - dc:seek(x, y + dy):pen(pen):string(txt) + dc:seek(x, y + dy) + :pen(happyPen):string(happiness or "") + :string((happiness and job) and " " or "") + :pen(pen):string(job or "") -- mark coordinates as used for j = 0, #txt - 1 do From 0f6aebae507ca9c2f17f6fd04756f9a489f8fc19 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Tue, 7 Jan 2025 23:39:43 +0100 Subject: [PATCH 295/811] fix GetScreenCoordinates for ASCII mode Also, simplify GetUnitJob --- gui/tooltips.lua | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index b54dc30481..c4fc628173 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -70,11 +70,8 @@ local function GetUnitHappiness(unit) end local function GetUnitJob(unit) - local job = unit.job - if job and job.current_job then - return dfhack.job.getName(job.current_job) - end - return nil + local job = unit.job.current_job + return job and dfhack.job.getName(job) end local function GetUnitNameAndJob(unit) @@ -169,7 +166,6 @@ end -- map coordinates -> interface layer coordinates function GetScreenCoordinates(map_coord) if not map_coord then return end - -- -> map viewport offset local vp = df.global.world.viewport local vp_Coord = vp.window_x -- is actually coord @@ -178,20 +174,25 @@ function GetScreenCoordinates(map_coord) y = map_coord.y - vp_Coord.y, z = map_coord.z - vp_Coord.z, } - -- -> pixel offset - local gps = df.global.gps - local map_tile_pixels = gps.viewport_zoom_factor // 4; - local screen_coord_px = { - x = map_tile_pixels * map_offset_by_vp.x, - y = map_tile_pixels * map_offset_by_vp.y, - } - -- -> interface layer coordinates - local screen_coord_text = { - x = math.ceil( screen_coord_px.x / gps.tile_pixel_x ), - y = math.ceil( screen_coord_px.y / gps.tile_pixel_y ), - } - return screen_coord_text + if not dfhack.screen.inGraphicsMode() then + return map_offset_by_vp + else + -- -> pixel offset + local gps = df.global.gps + local map_tile_pixels = gps.viewport_zoom_factor // 4; + local screen_coord_px = { + x = map_tile_pixels * map_offset_by_vp.x, + y = map_tile_pixels * map_offset_by_vp.y, + } + -- -> interface layer coordinates + local screen_coord_text = { + x = math.ceil( screen_coord_px.x / gps.tile_pixel_x ), + y = math.ceil( screen_coord_px.y / gps.tile_pixel_y ), + } + + return screen_coord_text + end end function TooltipsVizualizer:onRenderFrame(dc, rect) From be5bccd8a7f5951413782860a7f94adf609f524c Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Tue, 7 Jan 2025 23:51:44 +0100 Subject: [PATCH 296/811] tooltips.rst: add IMPORTANT NOTE as well as some clarifications --- docs/gui/tooltips.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/gui/tooltips.rst b/docs/gui/tooltips.rst index 673fc1087b..b01d076a26 100644 --- a/docs/gui/tooltips.rst +++ b/docs/gui/tooltips.rst @@ -5,7 +5,15 @@ gui/tooltips :summary: Show name and job tooltips near units on map. :tags: fort inspection -This script shows "tooltips" following units and/or mouse with job names. +**IMPORTANT NOTE**: the tooltips will show over any vanilla UI elements! + + +This script shows "tooltips" in two optional modes: + +* following the mouse, when a unit is underneath the cursor; +* following units on the map. + +Information shown includes happiness indicator, name, and current job. Usage ----- From 5e745df3f9263f55f4ae54b66936124e46190917 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Wed, 8 Jan 2025 21:51:45 -0600 Subject: [PATCH 297/811] RightClickOverlay: check focus before clearing selection_pos Check for the relevant focus before clearing .selection_pos. Otherwise, the overlay will erroneously consume a LEAVESCREEN or right-click input in the other modes when .selection_pos.x >= 0. Note: .selection_pos is (0,0,0) after DF starts from scratch. --- gui/design.lua | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index 22666d83fe..2279ec005a 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -143,9 +143,13 @@ RightClickOverlay.ATTRS{ function RightClickOverlay:onInput(keys) if keys._MOUSE_R or keys.LEAVESCREEN then -- building mode - if uibs.selection_pos.x >= 0 then - uibs.selection_pos:clear() - return true + if dfhack.gui.matchFocusString('dwarfmode/Building/Placement', + dfhack.gui.getDFViewscreen(true)) + then + if uibs.selection_pos.x >= 0 then + uibs.selection_pos:clear() + return true + end -- all other modes elseif selection_rect.start_x >= 0 then selection_rect.start_x = -30000 From 6dc147ce70c24da2d7fe05c77b3fdaa90fa5257d Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Thu, 9 Jan 2025 18:44:35 -0600 Subject: [PATCH 298/811] changelog for RightClickOverlay focus check --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 7d91af061e..1ab83703dd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -39,6 +39,7 @@ Template for new versions: - `fix/dry-buckets`: don't empty buckets for wells that are actively in use - `gui/unit-info-viewer`: skill progress bars now show correct XP thresholds for skills past Legendary+5 - `caravan`: no longer incorrectly identify wood-based plant items and plant-based soaps as being ethically unsuitable for trading with the elves +- `gui/design`: don't require an extra cancel input to exit or cancel designations ## Misc Improvements - `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking From def11659faa2bfabfbd089ec3458b74a4d1f3960 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 9 Jan 2025 18:10:46 -0800 Subject: [PATCH 299/811] allow units to be given nicknames on the embark screen --- changelog.txt | 1 + docs/gui/rename.rst | 3 +++ gui/rename.lua | 60 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+) diff --git a/changelog.txt b/changelog.txt index 7d91af061e..c553a53049 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: - `gui/settings-manager`: new overlay on the Labor -> Standing Orders tab for configuring the number of barrels to reserve for job use (so you can brew alcohol and not have all your barrels claimed by stockpiles for container storage) - `gui/settings-manager`: standing orders save/load now includes the reserved barrels setting - `gui/rename`: add overlay to worldgen screen allowing you to rename the world before the new world is saved +- `gui/rename`: add overlay to the "Prepare carefully" embark screen that transparently fixes a DF bug where you can't give units nicknames or custom professions ## Fixes - `fix/dry-buckets`: don't empty buckets for wells that are actively in use diff --git a/docs/gui/rename.rst b/docs/gui/rename.rst index 39b64c632a..56705b57c3 100644 --- a/docs/gui/rename.rst +++ b/docs/gui/rename.rst @@ -105,3 +105,6 @@ This tool supports the following overlays: ``gui/rename.world`` Adds a widget to the world generation screen for renaming the world. +``gui/rename.unit_embark`` + Transparently fixes DF :bug:`12060` on the "Prepare carefully" embark screen + where the player is unable to give units nicknames or custom professions. diff --git a/gui/rename.lua b/gui/rename.lua index 00ed9b0369..bb22f0ba4c 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -797,7 +797,67 @@ function WorldRenameOverlay:init() } end +-- +-- UnitEmbarkRenameOverlay +-- + +local mi = df.global.game.main_interface + +UnitEmbarkRenameOverlay = defclass(UnitEmbarkRenameOverlay, overlay.OverlayWidget) +UnitEmbarkRenameOverlay.ATTRS { + desc='Allows editing of unit nicknames on the embark preparation screen.', + default_enabled=true, + viewscreens='setupdwarfgame/Dwarves', + fullscreen=true, + active=function() return mi.view_sheets.open end, +} + +local function get_selected_embark_unit() + local scr = dfhack.gui.getDFViewscreen(true) + return scr.s_unit[scr.selected_u] +end + +function UnitEmbarkRenameOverlay:onInput(keys) + if (keys.SELECT or keys._STRING) and mi.view_sheets.unit_overview_customizing then + if mi.view_sheets.unit_overview_entering_nickname then + if keys.SELECT then + mi.view_sheets.unit_overview_entering_nickname = false + return true + end + local unit = get_selected_embark_unit() + if unit then + if keys._STRING == 0 then + unit.name.nickname = string.sub(unit.name.nickname, 1, -2) + else + unit.name.nickname = unit.name.nickname .. string.char(keys._STRING) + end + local hf = df.historical_figure.find(unit.hist_figure_id) + if hf then + hf.name.nickname = unit.name.nickname + end + return true + end + elseif mi.view_sheets.unit_overview_entering_profession_nickname then + if keys.SELECT then + mi.view_sheets.unit_overview_entering_profession_nickname = false + return true + end + local unit = get_selected_embark_unit() + if unit then + if keys._STRING == 0 then + unit.custom_profession = string.sub(unit.custom_profession, 1, -2) + else + unit.custom_profession = unit.custom_profession .. string.char(keys._STRING) + end + return true + end + end + end + return false +end + OVERLAY_WIDGETS = { + unit_embark=UnitEmbarkRenameOverlay, world=WorldRenameOverlay, } From fc358acf8cc753a5848a1740f0969973a4979e31 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 Jan 2025 14:40:08 -0800 Subject: [PATCH 300/811] changelog editing pass --- changelog.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index a87ddea0b8..97c2325dad 100644 --- a/changelog.txt +++ b/changelog.txt @@ -40,13 +40,13 @@ Template for new versions: - `fix/dry-buckets`: don't empty buckets for wells that are actively in use - `gui/unit-info-viewer`: skill progress bars now show correct XP thresholds for skills past Legendary+5 - `caravan`: no longer incorrectly identify wood-based plant items and plant-based soaps as being ethically unsuitable for trading with the elves -- `gui/design`: don't require an extra cancel input to exit or cancel designations +- `gui/design`: don't require an extra right click when canceling building area designations +- `gui/gm-unit`: refresh unit sprite when profession is changed ## Misc Improvements - `immortal-cravings`: goblins and other naturally non-eating/non-drinking races will now also satisfy their needs for eating and drinking - `caravan`: add filter for written works in display furniture assignment dialog - `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) -- `gui/gm-unit`: refresh unit sprite when profession is changed - `gui/sitemap`: show primary group affiliation for visitors and invaders (e.g. civilization name or performance troupe) ## Removed From bdb136707d0621f2e0f10af38962be43f7791e4a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 11 Jan 2025 15:39:29 -0800 Subject: [PATCH 301/811] expand the table as the window resizes --- gui/rename.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/rename.lua b/gui/rename.lua index bb22f0ba4c..362ba2f3c4 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -414,7 +414,7 @@ function Rename:init(info) }, }, }, - widgets.Panel{frame={t=0, l=31}, -- words table + widgets.Panel{frame={t=0, l=31, r=0}, -- words table subviews={ widgets.CycleHotkeyLabel{ view_id='sort', From 9484f8eee7a4b42da7c608d9db930385e5cac157 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 12 Jan 2025 11:22:50 +0100 Subject: [PATCH 302/811] vieport.window_x -> .coord --- gui/tooltips.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index c4fc628173..9db8dc72a2 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -168,7 +168,7 @@ function GetScreenCoordinates(map_coord) if not map_coord then return end -- -> map viewport offset local vp = df.global.world.viewport - local vp_Coord = vp.window_x -- is actually coord + local vp_Coord = vp.corner local map_offset_by_vp = { x = map_coord.x - vp_Coord.x, y = map_coord.y - vp_Coord.y, @@ -205,7 +205,7 @@ function TooltipsVizualizer:onRenderFrame(dc, rect) end local vp = df.global.world.viewport - local topleft = vp.window_x + local topleft = vp.corner local width = vp.max_x local height = vp.max_y local bottomright = {x = topleft.x + width, y = topleft.y + height, z = topleft.z} From 33dbe85f4e8d63c0dddf584b7c7fa11ef4d74e85 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 12 Jan 2025 11:28:49 +0100 Subject: [PATCH 303/811] use `getUnitsInBox(pos1, pos2)` overload --- gui/tooltips.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index 9db8dc72a2..671b8faf3b 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -85,9 +85,9 @@ local function GetUnitNameAndJob(unit) return table.concat(sb) end -local function GetTooltipText(x,y,z) +local function GetTooltipText(pos) local txt = {} - local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z) or {} -- todo: maybe (optionally) use filter parameter here? + local units = dfhack.units.getUnitsInBox(pos, pos) or {} -- todo: maybe (optionally) use filter parameter here? for _,unit in ipairs(units) do txt[#txt+1] = GetUnitNameAndJob(unit) @@ -136,7 +136,7 @@ function MouseTooltip:render(dc) if not x then return end local pos = dfhack.gui.getMousePos() - local text = GetTooltipText(pos2xyz(pos)) + local text = GetTooltipText(pos) if #text == 0 then return end self.label:setText(text) @@ -210,7 +210,7 @@ function TooltipsVizualizer:onRenderFrame(dc, rect) local height = vp.max_y local bottomright = {x = topleft.x + width, y = topleft.y + height, z = topleft.z} - local units = dfhack.units.getUnitsInBox(topleft.x,topleft.y,topleft.z,bottomright.x,bottomright.y,bottomright.z) or {} + local units = dfhack.units.getUnitsInBox(topleft, bottomright) or {} if #units == 0 then return end local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) From df41612c437185e5f256f5345fe7205a5125149c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 Jan 2025 03:47:43 -0800 Subject: [PATCH 304/811] use new translation module and fix a number of encoding bugs along the way --- armoks-blessing.lua | 2 +- brainwash.lua | 2 +- caravan.lua | 15 ++------ deathcause.lua | 20 +++++----- devel/kill-hf.lua | 28 +------------- devel/unit-path.lua | 2 +- diplomacy.lua | 8 ++-- do-job-now.lua | 19 ++-------- elevate-mental.lua | 14 +------ elevate-physical.lua | 12 +----- embark-anyone.lua | 2 +- emigration.lua | 2 +- exportlegends.lua | 28 +++++++------- fix/corrupt-equipment.lua | 2 +- fix/loyaltycascade.lua | 4 +- fix/stuck-merchants.lua | 29 ++------------ fix/stuck-worship.lua | 6 +-- fixnaked.lua | 2 +- gaydar.lua | 2 +- gui/advfort.lua | 2 +- gui/companion-order.lua | 2 +- gui/family-affairs.lua | 3 +- gui/gm-editor.lua | 2 +- gui/masspit.lua | 20 ++++++---- gui/rename.lua | 13 ++++--- gui/room-list.lua | 2 +- gui/sitemap.lua | 6 +-- gui/unit-info-viewer.lua | 2 +- gui/workshop-job.lua | 2 +- internal/advtools/convo.lua | 4 +- internal/confirm/specs.lua | 2 +- internal/exportlegends/racefilter.lua | 2 +- internal/gm-unit/editor_civilization.lua | 2 +- linger.lua | 6 +-- list-agreements.lua | 4 +- make-legendary.lua | 2 +- markdown.lua | 2 +- modtools/extra-gamelog.lua | 6 +-- modtools/set-belief.lua | 2 +- modtools/set-need.lua | 2 +- modtools/set-personality.lua | 2 +- necronomicon.lua | 4 +- pref-adjust.lua | 8 ++-- prefchange.lua | 48 ++++++++++++------------ set-orientation.lua | 2 +- superdwarf.lua | 7 +--- troubleshoot-item.lua | 12 +----- uniform-unstick.lua | 2 +- unretire-anyone.lua | 4 +- warn-stranded.lua | 2 +- 50 files changed, 135 insertions(+), 243 deletions(-) diff --git a/armoks-blessing.lua b/armoks-blessing.lua index 9a64c5bc96..e34cc9992e 100644 --- a/armoks-blessing.lua +++ b/armoks-blessing.lua @@ -233,7 +233,7 @@ end -- --------------------------------------------------------------------------- function adjust_all_dwarves(skillname) for _,v in ipairs(dfhack.units.getCitizens()) do - print("Adjusting "..dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(v)))) + print("Adjusting "..dfhack.df2console(dfhack.units.getReadableName(v))) brainwash_unit(v) elevate_attributes(v) rejuvenate.rejuvenate(v, true) diff --git a/brainwash.lua b/brainwash.lua index 12776de283..1cbdc29355 100644 --- a/brainwash.lua +++ b/brainwash.lua @@ -27,7 +27,7 @@ function brainwash_unit(profile) return end - unit_name=dfhack.TranslateName(dfhack.units.getVisibleName(unit)) + unit_name = dfhack.df2console(dfhack.units.getReadableName(unit)) print("Previous personality values for "..unit_name) printall(unit.status.current_soul.personality.traits) diff --git a/caravan.lua b/caravan.lua index 2d3a0cfb79..0dd2cf6d89 100644 --- a/caravan.lua +++ b/caravan.lua @@ -62,7 +62,7 @@ function commands.list() print(dfhack.df2console(('%d: %s caravan from %s'):format( id, df.creature_raw.find(df.historical_entity.find(car.entity).race).name[2], -- adjective - dfhack.TranslateName(df.historical_entity.find(car.entity).name) + dfhack.translation.translateName(df.historical_entity.find(car.entity).name) ))) print(' ' .. (df.caravan_state.T_trade_state[car.trade_state] or ('Unknown state: ' .. car.trade_state))) print((' %d day(s) remaining'):format(math.floor(car.time_remaining / 120))) @@ -126,15 +126,6 @@ local function isDisconnectedPackAnimal(unit) end end -local function getPrintableUnitName(unit) - local visible_name = dfhack.units.getVisibleName(unit) - local profession_name = dfhack.units.getProfessionName(unit) - if visible_name.has_name then - return ('%s (%s)'):format(dfhack.TranslateName(visible_name), profession_name) - end - return profession_name -- for unnamed animals -end - local function rejoin_pack_animals() print('Reconnecting disconnected pack animals...') local found = false @@ -142,8 +133,8 @@ local function rejoin_pack_animals() if unit.flags1.merchant and isDisconnectedPackAnimal(unit) then local dragger = unit.following print((' %s <-> %s'):format( - dfhack.df2console(getPrintableUnitName(unit)), - dfhack.df2console(getPrintableUnitName(dragger)) + dfhack.df2console(dfhack.units.getReadableName(unit)), + dfhack.df2console(dfhack.units.getReadableName(dragger)) )) unit.relationship_ids[ df.unit_relationship_type.Dragger ] = dragger.id dragger.relationship_ids[ df.unit_relationship_type.Draggee ] = unit.id diff --git a/deathcause.lua b/deathcause.lua index 9b0d4fad61..16e05d5f5a 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -25,13 +25,11 @@ function getDeathStringFromCause(cause) end function displayDeathUnit(unit) - local str = ("The %s"):format(getRaceNameSingular(unit.race)) - if unit.name.has_name then - str = str .. (" %s"):format(dfhack.TranslateName(unit.name)) - end + local str = unit.name.has_name and '' or 'The ' + str = str .. dfhack.units.getReadableName(unit) if not dfhack.units.isDead(unit) then - print(str .. " is not dead yet!") + print(dfhack.df2console(str) .. " is not dead yet!") return end @@ -46,13 +44,13 @@ function displayDeathUnit(unit) if killer then str = str .. (", killed by the %s"):format(getRaceNameSingular(killer.race)) if killer.name.has_name then - str = str .. (" %s"):format(dfhack.TranslateName(killer.name)) + str = str .. (" %s"):format(dfhack.translation.translateName(dfhack.units.getVisibleName(killer))) end end end end - print(str .. '.') + print(dfhack.df2console(str) .. '.') end -- returns the item description if the item still exists; otherwise @@ -68,7 +66,7 @@ end function displayDeathEventHistFigUnit(histfig_unit, event) local str = ("The %s %s %s in year %d"):format( getRaceNameSingular(histfig_unit.race), - dfhack.TranslateName(histfig_unit.name), + dfhack.translation.translateName(dfhack.units.getVisibleName(histfig_unit)), getDeathStringFromCause(event.death_cause), event.year ) @@ -77,7 +75,7 @@ function displayDeathEventHistFigUnit(histfig_unit, event) if slayer_histfig then str = str .. (", killed by the %s %s"):format( getRaceNameSingular(slayer_histfig.race), - dfhack.TranslateName(slayer_histfig.name) + dfhack.translation.translateName(dfhack.units.getVisiblename(slayer_histfig)) ) end @@ -89,7 +87,7 @@ function displayDeathEventHistFigUnit(histfig_unit, event) end end - print(str .. '.') + print(dfhack.df2console(str) .. '.') end -- Returns the death event for the given histfig or nil if not found @@ -111,7 +109,7 @@ function displayDeathHistFig(histfig) end if not dfhack.units.isDead(histfig_unit) then - print(("%s is not dead yet!"):format(dfhack.TranslateName(histfig_unit.name))) + print(("%s is not dead yet!"):format(dfhack.df2console(dfhack.units.getReadableName(histfig_unit)))) else local death_event = getDeathEventForHistFig(histfig.id) displayDeathEventHistFigUnit(histfig_unit, death_event) diff --git a/devel/kill-hf.lua b/devel/kill-hf.lua index e2cf5f7215..63b8083988 100644 --- a/devel/kill-hf.lua +++ b/devel/kill-hf.lua @@ -1,31 +1,5 @@ -- Kills the specified historical figure ---[====[ - -devel/kill-hf -============= - -Kills the specified historical figure, even if off-site, or terminates a -pregnancy. Useful for working around :bug:`11549`. - -Usage:: - - devel/kill-hf [-p|--pregnancy] [-n|--dry-run] HISTFIG_ID - -Arguments: - -``histfig_id``: - the ID of the historical figure to target - -``-p``, ``--pregnancy``: - if specified, and if the historical figure is pregnant, terminate the - pregnancy instead of killing the historical figure - -``-n``, ``--dry-run``: - if specified, only print the name of the historical figure - -]====] - local target_hf = -1 local target_pregnancy = false local dry_run = false @@ -44,7 +18,7 @@ end local hf = df.historical_figure.find(target_hf) or qerror('histfig not found: ' .. target_hf) -local hf_name = dfhack.df2console(dfhack.TranslateName(hf.name)) +local hf_name = dfhack.df2console(dfhack.translation.translateName(hf.name)) local hf_desc = ('%i: %s (%s)'):format(target_hf, hf_name, dfhack.units.getRaceNameById(hf.race)) if dry_run then diff --git a/devel/unit-path.lua b/devel/unit-path.lua index 459ff92159..6f5e34d58e 100644 --- a/devel/unit-path.lua +++ b/devel/unit-path.lua @@ -127,7 +127,7 @@ function UnitPathUI:onRenderBody(dc) dc:seek(2,3):pen(COLOR_BLUE):string(prof) if name and name.has_name then - dc:seek(2,4):pen(COLOR_BLUE):string(dfhack.TranslateName(name)) + dc:seek(2,4):pen(COLOR_BLUE):string(dfhack.translation.translateName(name)) end local cursor = guidm.getCursorPos() diff --git a/diplomacy.lua b/diplomacy.lua index 7baee75ab6..7b44efa4cf 100644 --- a/diplomacy.lua +++ b/diplomacy.lua @@ -25,10 +25,10 @@ function get_civ_list() end end table.insert(civ_list, { - cur_civ_id, - rel_str, - matched, - dfhack.TranslateName(cur_civ.name, true) + cur_civ_id, + rel_str, + matched, + dfhack.translation.translateName(cur_civ.name, true) }) end end diff --git a/do-job-now.lua b/do-job-now.lua index 4e83970c8f..3ef9585a9b 100644 --- a/do-job-now.lua +++ b/do-job-now.lua @@ -6,16 +6,6 @@ local function print_help() print(dfhack.script_help()) end -local function getUnitName(unit) - local language_name = dfhack.units.getVisibleName(unit) - if language_name.has_name then - return dfhack.df2console(dfhack.TranslateName( language_name )) - end - - -- animals - return dfhack.units.getProfessionName(unit) -end - local function doJobNow(job) local job_str = dfhack.job.getName(job) if not job.flags.do_now then @@ -30,7 +20,7 @@ local function doJobNow(job) end local unit = dfhack.job.getWorker(job) if unit then - print("... by " .. getUnitName(unit) ) + print("... by " .. dfhack.df2console(dfhack.units.getReadableName(unit))) end end @@ -64,7 +54,6 @@ end local function doUnitJobNow(unit) if dfhack.units.isCitizen(unit) then - --print('This will attempt to make a job of ' .. getUnitName(unit) .. ' a top priority') local t_job = unit.job if t_job then local job = t_job.current_job @@ -73,10 +62,8 @@ local function doUnitJobNow(unit) return end end - print("Couldn't find any job for " .. getUnitName(unit) ) + print("Couldn't find any job for " .. dfhack.df2console(dfhack.units.getReadableName(unit))) else - --print('This will attempt to make a job with ' .. getUnitName(unit) .. ' a top priority') - local needle = unit.id for _link, job in utils.listpairs(df.global.world.jobs.list) do if #job.general_refs > 0 then @@ -92,7 +79,7 @@ local function doUnitJobNow(unit) end end - print("Couldn't find any job involving " .. getUnitName(unit) ) + print("Couldn't find any job involving " .. dfhack.df2console(dfhack.units.getReadableName(unit))) end end diff --git a/elevate-mental.lua b/elevate-mental.lua index c484a22635..cd067a60eb 100644 --- a/elevate-mental.lua +++ b/elevate-mental.lua @@ -1,14 +1,4 @@ -- Elevate all the mental attributes of a unit --- by vjek -local help = [====[ - -elevate-mental -============== -Set all mental attributes of the selected dwarf to the maximum possible, or -any number numbers between 0 and 5000 passed as an argument: -``elevate-mental 100`` for example would make the dwarf very stupid indeed. - -]====] function ElevateMentalAttributes(value) local unit=dfhack.gui.getSelectedUnit() @@ -17,7 +7,7 @@ function ElevateMentalAttributes(value) return end --print name of dwarf - print("Adjusting "..dfhack.TranslateName(dfhack.units.getVisibleName(unit))) + print("Adjusting "..dfhack.df2console(dfhack.units.getReadableName(unit))) --walk through available attributes, adjust current to max if unit.status.current_soul then for k,v in pairs(unit.status.current_soul.mental_attrs) do @@ -42,7 +32,7 @@ if opt ~= nil then ElevateMentalAttributes(opt) end if opt <0 or opt >5000 then - print(help) + print(dfhack.script_help()) print('\n\nInvalid number!') end end diff --git a/elevate-physical.lua b/elevate-physical.lua index bf8e97c458..6a6aeffdb4 100644 --- a/elevate-physical.lua +++ b/elevate-physical.lua @@ -1,14 +1,4 @@ -- Elevate all the physical attributes of a unit --- by vjek ---[====[ - -elevate-physical -================ -Set all physical attributes of the selected dwarf to the maximum possible, or -any number numbers between 0 and 5000 passed as an argument. Higher is -usually better, but an ineffective hammerer can be useful too... - -]====] function ElevatePhysicalAttributes(value) local unit=dfhack.gui.getSelectedUnit() @@ -17,7 +7,7 @@ function ElevatePhysicalAttributes(value) return end --print name of dwarf - print("Adjusting "..dfhack.TranslateName(dfhack.units.getVisibleName(unit))) + print("Adjusting "..dfhack.df2console(dfhack.units.getReadableName(unit))) --walk through available attributes, adjust current to max if unit.body then for k,v in pairs(unit.body.physical_attrs) do diff --git a/embark-anyone.lua b/embark-anyone.lua index 0f6fc61ad2..10772e46e1 100644 --- a/embark-anyone.lua +++ b/embark-anyone.lua @@ -68,7 +68,7 @@ function embarkAnyone() -- Find the civ's name, or come up with one if civ.name.has_name then - label = dfhack.TranslateName(civ.name, true) .. "\n" + label = dfhack.translation.translateName(civ.name, true) .. "\n" else label = "Unnamed " .. dfhack.units.getRaceReadableNameById(civ.race) .. diff --git a/emigration.lua b/emigration.lua index 1d1dea305e..ea494a7e36 100644 --- a/emigration.lua +++ b/emigration.lua @@ -34,7 +34,7 @@ end function desert(u,method,civ) u.following = nil - local line = dfhack.TranslateName(dfhack.units.getVisibleName(u)) .. " has " + local line = dfhack.units.getReadableName(u) .. " has " if method == 'merchant' then line = line.."joined the merchants" u.flags1.merchant = true diff --git a/exportlegends.lua b/exportlegends.lua index db5e12ffb1..e4c5eedea4 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -136,8 +136,8 @@ local function export_more_legends_xml() file:write("\n") file:write("\n") - file:write(""..escape_xml(dfhack.df2utf(dfhack.TranslateName(world.world_data.name))).."\n") - file:write(""..escape_xml(dfhack.df2utf(dfhack.TranslateName(world.world_data.name,1))).."\n") + file:write(""..escape_xml(dfhack.df2utf(dfhack.translation.translateName(world.world_data.name))).."\n") + file:write(""..escape_xml(dfhack.df2utf(dfhack.translation.translateName(world.world_data.name,1))).."\n") local chunks = {} @@ -145,7 +145,7 @@ local function export_more_legends_xml() for landmassK, landmassV in progress_ipairs(vector, 'landmasses') do file:write("\t\n") file:write("\t\t"..landmassV.index.."\n") - file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(landmassV.name,1))).."\n") + file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(landmassV.name,1))).."\n") file:write("\t\t"..landmassV.min_x..","..landmassV.min_y.."\n") file:write("\t\t"..landmassV.max_x..","..landmassV.max_y.."\n") file:write("\t\n") @@ -156,7 +156,7 @@ local function export_more_legends_xml() for mountainK, mountainV in progress_ipairs(vector, 'mountains') do file:write("\t\n") file:write("\t\t"..mountainK.."\n") - file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(mountainV.name,1))).."\n") + file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(mountainV.name,1))).."\n") file:write("\t\t"..mountainV.pos.x..","..mountainV.pos.y.."\n") file:write("\t\t"..mountainV.height.."\n") if mountainV.flags.is_volcano then @@ -205,7 +205,7 @@ local function export_more_legends_xml() table.insert(chunks, make_chunk('rivers', world.world_data.rivers, function(vector) for riverK, riverV in progress_ipairs(vector, 'rivers') do file:write("\t\n") - file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(riverV.name, 1))).."\n") + file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(riverV.name, 1))).."\n") file:write("\t\t") for pathK, pathV in progress_ipairs(riverV.path.x, 'river section', true) do file:write(pathV..","..riverV.path.y[pathK]..",") @@ -249,8 +249,8 @@ local function export_more_legends_xml() file:write("\t\t\t\t"..buildingV.id.."\n") file:write("\t\t\t\t"..df_enums.abstract_building_type[buildingV:getType()]:lower().."\n") if table_containskey(buildingV,"name") then - file:write("\t\t\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(buildingV.name, 1))).."\n") - file:write("\t\t\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(buildingV.name))).."\n") + file:write("\t\t\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(buildingV.name, 1))).."\n") + file:write("\t\t\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(buildingV.name))).."\n") end if df.abstract_building_templest:is_instance(buildingV) then file:write("\t\t\t\t"..buildingV.deity_type.."\n") @@ -280,7 +280,7 @@ local function export_more_legends_xml() for wcK, wcV in progress_ipairs(vector, 'constructions') do file:write("\t\n") file:write("\t\t"..wcV.id.."\n") - file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(wcV.name,1))).."\n") + file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(wcV.name,1))).."\n") file:write("\t\t"..(df_enums.world_construction_type[wcV:getType()]):lower().."\n") file:write("\t\t") for xK, xVal in ipairs(wcV.square_pos.x) do @@ -338,7 +338,7 @@ local function export_more_legends_xml() for idK, idV in progress_ipairs(vector, 'identities') do file:write("\t\n") file:write("\t\t"..idV.id.."\n") - file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(idV.name,1))).."\n") + file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(idV.name,1))).."\n") local id_tag = df.identity_type.attrs[idV.type].id_tag if id_tag then file:write("\t\t<"..id_tag..">"..idV[id_tag].."\n") @@ -446,7 +446,7 @@ local function export_more_legends_xml() for occasionK, occasionV in pairs(entityV.occasion_info.occasions) do file:write("\t\t\n") file:write("\t\t\t"..occasionV.id.."\n") - file:write("\t\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(occasionV.name,1))).."\n") + file:write("\t\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(occasionV.name,1))).."\n") file:write("\t\t\t"..occasionV.purpose_id.."\n") for scheduleK, scheduleV in pairs(occasionV.schedule) do file:write("\t\t\t\n") @@ -760,7 +760,7 @@ local function export_more_legends_xml() else dfhack.printerr ("Unknown df.identity_type value encountered:"..tostring (identity.type)..". Please report to DFHack team.") end - file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(identity.name))).."\n") + file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(identity.name))).."\n") local craw = df.creature_raw.find(identity.race) if craw then file:write("\t\t"..(craw.creature_id):lower().."\n") @@ -967,7 +967,7 @@ local function export_more_legends_xml() for formK, formV in progress_ipairs(vector, 'poetic forms') do file:write("\t\n") file:write("\t\t"..formV.id.."\n") - file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(formV.name,1))).."\n") + file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(formV.name,1))).."\n") file:write("\t\n") end end)) @@ -976,7 +976,7 @@ local function export_more_legends_xml() for formK, formV in progress_ipairs(vector, 'musical forms') do file:write("\t\n") file:write("\t\t"..formV.id.."\n") - file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(formV.name,1))).."\n") + file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(formV.name,1))).."\n") file:write("\t\n") end end)) @@ -985,7 +985,7 @@ local function export_more_legends_xml() for formK, formV in progress_ipairs(vector, 'dance forms') do file:write("\t\n") file:write("\t\t"..formV.id.."\n") - file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.TranslateName(formV.name,1))).."\n") + file:write("\t\t"..escape_xml(dfhack.df2utf(dfhack.translation.translateName(formV.name,1))).."\n") file:write("\t\n") end end)) diff --git a/fix/corrupt-equipment.lua b/fix/corrupt-equipment.lua index e509b8d9ec..a11723ee06 100644 --- a/fix/corrupt-equipment.lua +++ b/fix/corrupt-equipment.lua @@ -85,7 +85,7 @@ function fix_equipment () for i, squad in ipairs (df.global.world.squads.all) do if squad.entity_id == df.global.plotinfo.group_id then - local squad_name = dfhack.TranslateName(squad.name, true) + local squad_name = dfhack.translation.translateName(squad.name, true) if squad.alias ~= "" then squad_name = squad.alias end diff --git a/fix/loyaltycascade.lua b/fix/loyaltycascade.lua index b6ae0515ef..768fbdfcf2 100644 --- a/fix/loyaltycascade.lua +++ b/fix/loyaltycascade.lua @@ -51,7 +51,7 @@ local function fixUnit(unit) -- If the unit is a former member of your civilization, as well as now an -- enemy of it, we make it become a member again. if former_civ_index and enemy_civ_index then - local civ_name = dfhack.TranslateName(df.historical_entity.find(df.global.plotinfo.civ_id).name) + local civ_name = dfhack.translation.translateName(df.historical_entity.find(df.global.plotinfo.civ_id).name) convertUnit(unit, df.global.plotinfo.civ_id, former_civ_index, enemy_civ_index) @@ -62,7 +62,7 @@ local function fixUnit(unit) end if former_group_index and enemy_group_index then - local group_name = dfhack.TranslateName(df.historical_entity.find(df.global.plotinfo.group_id).name) + local group_name = dfhack.translation.translateName(df.historical_entity.find(df.global.plotinfo.group_id).name) convertUnit(unit, df.global.plotinfo.group_id, former_group_index, enemy_group_index) diff --git a/fix/stuck-merchants.lua b/fix/stuck-merchants.lua index ceddc205e1..fa9b5b77c0 100644 --- a/fix/stuck-merchants.lua +++ b/fix/stuck-merchants.lua @@ -1,26 +1,7 @@ --- Dismisses stuck merchants that haven't entered the map yet --- Based on "dismissmerchants" by PatrikLundell: --- http://www.bay12forums.com/smf/index.php?topic=159297.msg7257447#msg7257447 - -local help = [====[ - -fix/stuck-merchants -=================== - -Dismisses merchants that haven't entered the map yet. This can fix :bug:`9593`. -This script should probably not be run if any merchants are on the map, so using -it with `repeat` is not recommended. - -Run ``fix/stuck-merchants -n`` or ``fix/stuck-merchants --dry-run`` to list all -merchants that would be dismissed but make no changes. - -]====] - - function getEntityName(u) local civ = df.historical_entity.find(u.civ_id) if not civ then return 'unknown civ' end - return dfhack.TranslateName(civ.name) + return dfhack.translation.translateName(civ.name) end function getEntityRace(u) @@ -38,7 +19,7 @@ function dismissMerchants(args) local dry_run = false for _, arg in pairs(args) do if args[1]:match('-h') or args[1]:match('help') then - print(help) + print(dfhack.script_help()) return elseif args[1]:match('-n') or args[1]:match('dry') then dry_run = true @@ -49,7 +30,7 @@ function dismissMerchants(args) print(('%s unit %d: %s (%s), civ %d (%s, %s)'):format( dry_run and 'Would remove' or 'Removing', u.id, - dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(u))), + dfhack.df2console(dfhack.units.getReadableName(u)), df.creature_raw.find(u.race).name[0], u.civ_id, dfhack.df2console(getEntityName(u)), @@ -62,6 +43,4 @@ function dismissMerchants(args) end end -if not dfhack_flags.module then - dismissMerchants{...} -end +dismissMerchants{...} diff --git a/fix/stuck-worship.lua b/fix/stuck-worship.lua index 2d48809141..d21df82717 100644 --- a/fix/stuck-worship.lua +++ b/fix/stuck-worship.lua @@ -87,10 +87,6 @@ local function get_prayer_targets(unit) end end -local function unit_name(unit) - return dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) -end - local count = 0 for _,unit in ipairs(dfhack.units.getCitizens(false, true)) do local prayer_targets = get_prayer_targets(unit) @@ -101,7 +97,7 @@ for _,unit in ipairs(dfhack.units.getCitizens(false, true)) do if shuffle_prayer_needs(needs, prayer_targets) then count = count + 1 if verbose then - print('Shuffled prayer target for '..unit_name(unit)) + print('Shuffled prayer target for '..dfhack.df2console(dfhack.units.getReadableName(unit))) end end ::next_unit:: diff --git a/fixnaked.lua b/fixnaked.lua index 916d8f5ad3..02dbc794f3 100644 --- a/fixnaked.lua +++ b/fixnaked.lua @@ -29,7 +29,7 @@ for fnUnitCount,fnUnit in ipairs(dfhack.units.getCitizens()) do end if fixed then total_fixed = total_fixed + 1 - print(total_fixed, total_removed, dfhack.TranslateName(dfhack.units.getVisibleName(fnUnit))) + print(total_fixed, total_removed, dfhack.df2console(dfhack.units.getReadableName(fnUnit))) end end end diff --git a/gaydar.lua b/gaydar.lua index 3981fd21e9..5dfb5a1828 100644 --- a/gaydar.lua +++ b/gaydar.lua @@ -77,7 +77,7 @@ end local function nameOrSpeciesAndNumber(unit) if unit.name.has_name then - return dfhack.TranslateName(dfhack.units.getVisibleName(unit))..' '..getSexString(unit.sex),true + return dfhack.translation.translateName(dfhack.units.getVisibleName(unit))..' '..getSexString(unit.sex),true else return 'Unit #'..unit.id..' ('..df.creature_raw.find(unit.race).caste[unit.caste].caste_name[0]..' '..getSexString(unit.sex)..')',false end diff --git a/gui/advfort.lua b/gui/advfort.lua index 7f17afb8fe..125bed7ba8 100644 --- a/gui/advfort.lua +++ b/gui/advfort.lua @@ -1197,7 +1197,7 @@ function usetool:update_site() local site_label=self.subviews.siteLabel --as:wid.Label if site then - site_label:itemById("site").text=dfhack.TranslateName(site.name) + site_label:itemById("site").text=dfhack.translation.translateName(site.name) else if settings.safe then site_label:itemById("site").text="" diff --git a/gui/companion-order.lua b/gui/companion-order.lua index 9659c79785..71bc644c22 100644 --- a/gui/companion-order.lua +++ b/gui/companion-order.lua @@ -494,7 +494,7 @@ function CompanionUi:onRenderBody( dc) else dc:pen(COLOR_GREY) end - dc:newline(1):string(string.char(k+char_a)..". "):string(dfhack.TranslateName(v.name)); + dc:newline(1):string(string.char(k+char_a)..". "):string(dfhack.translation.translateName(v.name)); end dc:pen(COLOR_GREY) local w,h=self:getWindowSize() diff --git a/gui/family-affairs.lua b/gui/family-affairs.lua index c0a64447b5..d67c9c0c0f 100644 --- a/gui/family-affairs.lua +++ b/gui/family-affairs.lua @@ -145,7 +145,8 @@ local function set_spouse(unit1, unit2, accept_fn) add_hf_link(df.histfig_hf_link_spousest, unit1.hist_figure_id, unit2.hist_figure_id) add_hf_link(df.histfig_hf_link_spousest, unit2.hist_figure_id, unit1.hist_figure_id) dfhack.gui.showAutoAnnouncement(df.announcement_type.MARRIAGE, xyz2pos(dfhack.units.getPosition(unit1)), - ('%s and %s have married!'):format(dfhack.TranslateName(unit1.name), dfhack.TranslateName(unit2.name)), + ('%s and %s have married!'):format(dfhack.translation.translateName(unit1.name), + dfhack.translation.translateName(unit2.name)), COLOR_LIGHTMAGENTA) accept_fn() end diff --git a/gui/gm-editor.lua b/gui/gm-editor.lua index 6d37f96c5f..6902875b69 100644 --- a/gui/gm-editor.lua +++ b/gui/gm-editor.lua @@ -636,7 +636,7 @@ function GmEditorUi:getStringValue(trg, field) elseif df.coord2d:is_instance(f) then text=('(%d, %d) %s'):format(f.x, f.y, text) elseif df.language_name:is_instance(f) then - text=('%s (%s) %s'):format(dfhack.TranslateName(f, false), dfhack.TranslateName(f, true), text) + text=('%s (%s) %s'):format(dfhack.translation.translateName(f, false), dfhack.translation.translateName(f, true), text) end end local enum = f._type diff --git a/gui/masspit.lua b/gui/masspit.lua index cc83ee847e..e46712819a 100644 --- a/gui/masspit.lua +++ b/gui/masspit.lua @@ -73,16 +73,16 @@ function Masspit:showStockpiles() for _, zone in pairs(df.global.world.buildings.other.STOCKPILE) do if (#zone.settings.animals.enabled > 0) then if #getCagedUnits(zone) > 0 then - local zone_name = zone.name.length and dfhack.TranslateName(zone.name) or "Animal stockpile #" .. zone.stockpile_number + local zone_name = #zone.name > 0 and zone.name or ("Animal stockpile #" .. zone.stockpile_number) local zone_position = df.coord:new() zone_position.x = zone.centerx zone_position.y = zone.centery zone_position.z = zone.z table.insert(choices, { - text = ([[%s: %s x:%s y:%s]]):format(zone.id, zone_name, zone.centerx, zone.centery), + text = zone_name, zone_position = zone_position, - zone_id = zone.id + zone_id = zone.id, }) end end @@ -97,6 +97,10 @@ function Masspit:showStockpiles() end end +local function getElevation() + return df.global.world.map.region_z + df.global.window_z - 100 +end + function Masspit:showPondPits() self.subviews.pages:setSelected('pits') @@ -104,14 +108,14 @@ function Masspit:showPondPits() for _, zone in pairs(df.global.world.buildings.other.ACTIVITY_ZONE) do if (zone.type == df.civzone_type.Pond) then - local zone_name = zone.name.length and dfhack.TranslateName(zone.name) or "Unnamed pit/pond" + local zone_name = #zone.name > 0 and zone.name or ("Unnamed pit/pond on elevation "..getElevation()) local zone_position = df.coord:new() zone_position.x = zone.centerx zone_position.y = zone.centery zone_position.z = zone.z table.insert(choices, { - text = ([[%s: %s x:%s y:%s]]):format(zone.id, zone_name, zone.centerx, zone.centery), + text = zone_name, zone_position = zone_position, zone_id = zone.id }) @@ -161,7 +165,7 @@ function Masspit:setPit(_, choice) for _, unit_id in pairs(self.caged_units) do local unit = df.unit.find(unit_id) - local unit_name = unit.name.has_name and dfhack.TranslateName(unit.name) or dfhack.units.getRaceNameById(unit.race) + local unit_name = dfhack.units.getReadableName(unit) -- Prevents duplicate units in assignments, which can cause crashes. local duplicate = false @@ -171,7 +175,7 @@ function Masspit:setPit(_, choice) end end - table.insert(choices, { text = ([[%s: %s %s]]):format(unit.id, unit_name, duplicate and "(ALREADY ASSIGNED)" or "") }) + table.insert(choices, { text = ([[%s: %s%s]]):format(unit.id, unit_name, duplicate and " (ALREADY ASSIGNED)" or "") }) if not duplicate then unit.general_refs:insert("#", { new = df.general_ref_building_civzone_assignedst, building_id=self.pit.id }) @@ -220,7 +224,7 @@ function MasspitScreen:onDismiss() view = nil end -if not dfhack.isMapLoaded() then +if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then qerror('This script requires a fortress map to be loaded') end diff --git a/gui/rename.lua b/gui/rename.lua index 362ba2f3c4..8f4cfe3c6d 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -65,7 +65,8 @@ local function get_world_target() local sync_targets = { function() df.global.world.cur_savegame.world_header.world_name = - ('%s, "%s"'):format(dfhack.TranslateName(target), dfhack.TranslateName(target, true)) + ('%s, "%s"'):format(dfhack.translation.translateName(target), + dfhack.translation.translateName(target, true)) end } return target, sync_targets @@ -93,7 +94,7 @@ local function select_location(site, cb) local desc, pen = sitemap.get_location_desc(loc) table.insert(choices, { text={ - dfhack.TranslateName(loc.name, true), + dfhack.translation.translateName(loc.name, true), ' (', {text=desc, pen=pen}, ')', @@ -312,11 +313,11 @@ function Rename:init(info) -- }, widgets.Label{ frame={t=2}, - text={{pen=COLOR_YELLOW, text=function() return pad_text(dfhack.TranslateName(self.target), self.frame_body.width) end}}, + text={{pen=COLOR_YELLOW, text=function() return pad_text(dfhack.translation.translateName(self.target), self.frame_body.width) end}}, }, widgets.Label{ frame={t=3}, - text={{pen=COLOR_LIGHTCYAN, text=function() return pad_text(('"%s"'):format(dfhack.TranslateName(self.target, true)), self.frame_body.width) end}}, + text={{pen=COLOR_LIGHTCYAN, text=function() return pad_text(('"%s"'):format(dfhack.translation.translateName(self.target, true)), self.frame_body.width) end}}, }, widgets.CycleHotkeyLabel{ view_id='language', @@ -621,8 +622,8 @@ function Rename:randomize_component_word(comp) end function Rename:generate_random_name() - print('TODO: call dfhack.GenerateName API once it exists') - -- dfhack.GenerateName(self.target) + print('TODO: call dfhack.translation.generateName API once it exists') + -- dfhack.translation.generateName(self.target) -- for _, sync_target in ipairs(self.sync_targets) do -- if type(sync_target) == 'function' then -- sync_target() diff --git a/gui/room-list.lua b/gui/room-list.lua index 00de972a10..4a9a50e93d 100644 --- a/gui/room-list.lua +++ b/gui/room-list.lua @@ -120,7 +120,7 @@ function drawUnitName(dc, unit) local vname = dfhack.units.getVisibleName(unit) if vname and vname.has_name then - dc:string(dfhack.TranslateName(vname)..', ') + dc:string(dfhack.translation.translateName(vname)..', ') end dc:string(dfhack.units.getProfessionName(unit)) else diff --git a/gui/sitemap.lua b/gui/sitemap.lua index ec7ffff7d1..1f779bfc9c 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -38,7 +38,7 @@ function get_location_desc(loc) local entity = is_deity and df.historical_figure.find(id) or df.historical_entity.find(id) local desc = 'Temple' if not entity then return desc, COLOR_YELLOW end - local name = dfhack.TranslateName(entity.name, true) + local name = dfhack.translation.translateName(entity.name, true) if #name > 0 then desc = ('%s to %s'):format(desc, name) end @@ -50,7 +50,7 @@ end local function get_location_label(loc, zones) local tokens = {} - table.insert(tokens, dfhack.TranslateName(loc.name, true)) + table.insert(tokens, dfhack.translation.translateName(loc.name, true)) local desc, pen = get_location_desc(loc) if desc then table.insert(tokens, ' (') @@ -106,7 +106,7 @@ end local function get_affiliation(unit) local he = df.historical_entity.find(unit.civ_id) if not he then return 'Unknown affiliation' end - local et_name = dfhack.TranslateName(he.name, true) + local et_name = dfhack.translation.translateName(he.name, true) local et_type = df.historical_entity_type[he.type]:gsub('(%l)(%u)', '%1 %2') return ('%s%s %s'):format(#et_name > 0 and et_name or 'Unknown', #et_name > 0 and ',' or '', et_type) end diff --git a/gui/unit-info-viewer.lua b/gui/unit-info-viewer.lua index ed677453a9..d6c817e69b 100644 --- a/gui/unit-info-viewer.lua +++ b/gui/unit-info-viewer.lua @@ -180,7 +180,7 @@ local function get_name_chunk(unit) end local function get_translated_name_chunk(unit) - local tname = dfhack.TranslateName(dfhack.units.getVisibleName(unit), true) + local tname = dfhack.translation.translateName(dfhack.units.getVisibleName(unit), true) if #tname == 0 then return '' end return ('"%s"'):format(tname) end diff --git a/gui/workshop-job.lua b/gui/workshop-job.lua index 5c368f8ba8..b071ee1142 100644 --- a/gui/workshop-job.lua +++ b/gui/workshop-job.lua @@ -75,7 +75,7 @@ function JobDetails:init(args) if self.job.flags.suspend then status = { text = 'Suspended', pen = COLOR_RED } elseif worker then - status = { text = dfhack.TranslateName(dfhack.units.getVisibleName(worker)), pen = COLOR_GREEN } + status = { text = dfhack.units.getReadableName(worker), pen = COLOR_GREEN } end self:addviews{ diff --git a/internal/advtools/convo.lua b/internal/advtools/convo.lua index 96ed0f015b..d8b9b06a37 100644 --- a/internal/advtools/convo.lua +++ b/internal/advtools/convo.lua @@ -77,11 +77,11 @@ local function new_choice(choice_type, title, keywords) end local function addWhereaboutsChoice(race, name, target_id, heard_of) - local title = "Ask for the whereabouts of the " .. race .. " " .. dfhack.TranslateName(name, true) + local title = "Ask for the whereabouts of the " .. race .. " " .. dfhack.translation.translateName(name, true) if heard_of then title = title .. " (Heard of)" end - local choice = new_choice(df.talk_choice_type.AskWhereabouts, title, dfhack.TranslateName(name):split()) + local choice = new_choice(df.talk_choice_type.AskWhereabouts, title, dfhack.translation.translateName(name):split()) -- insert before the last choice, which is usually "back" adventure.conversation.conv_choice_info:insert(#adventure.conversation.conv_choice_info-1, choice) choice.choice.invocation_target_hfid = target_id diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index b5e356bf72..3fb9ed13c4 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -424,7 +424,7 @@ ConfirmSpec{ selected_pos = scroll_pos + (y - first_portrait_rect.y1) // 3 end local unit = dfhack.gui.getWidget(scroll_rows, selected_pos, 0).u - selected_convict_name = dfhack.TranslateName(dfhack.units.getVisibleName(unit)) + selected_convict_name = dfhack.translation.translateName(dfhack.units.getVisibleName(unit)) if selected_convict_name == '' then selected_convict_name = 'this creature' end diff --git a/internal/exportlegends/racefilter.lua b/internal/exportlegends/racefilter.lua index 52524a9b71..7270396c23 100644 --- a/internal/exportlegends/racefilter.lua +++ b/internal/exportlegends/racefilter.lua @@ -89,7 +89,7 @@ local function do_filter(scr, filter_str, full_refresh) hfid_to_race[hfid] = hf and hf.race or -1 hfid_to_name[hfid] = hf and dfhack.toSearchNormalized( - ('%s %s'):format(dfhack.TranslateName(hf.name, false), dfhack.TranslateName(hf.name, true))) or '' + ('%s %s'):format(dfhack.translation.translateName(hf.name, false), dfhack.translation.translateName(hf.name, true))) or '' end if cur_race >= 0 and hfid_to_race[hfid] ~= cur_race then scr.histfigs_filtered:erase(idx) diff --git a/internal/gm-unit/editor_civilization.lua b/internal/gm-unit/editor_civilization.lua index c062814156..b1c6c0572a 100644 --- a/internal/gm-unit/editor_civilization.lua +++ b/internal/gm-unit/editor_civilization.lua @@ -66,7 +66,7 @@ function civ_name(id,format_name,format_no_name,name_other,name_invalid) return name_invalid or "" end end - local t={NAME=dfhack.TranslateName(civ.name),ENGLISH=dfhack.TranslateName(civ.name,true),ID=civ.id} --TODO race?, maybe something from raws? + local t={NAME=dfhack.translation.translateName(civ.name),ENGLISH=dfhack.translation.translateName(civ.name,true),ID=civ.id} --TODO race?, maybe something from raws? if t.NAME=="" then return string.gsub(format_no_name or " ($ID)", "%$(%w+)", t) end diff --git a/linger.lua b/linger.lua index 2207c48d1c..1e618827fc 100644 --- a/linger.lua +++ b/linger.lua @@ -32,11 +32,7 @@ end if not slayer then qerror("Killer not found!") elseif slayer.flags2.killed then - local slayerName = "" - if slayer.name.has_name then - slayerName = ", " .. dfhack.TranslateName(slayer.name) .. "," - end - qerror("Your slayer" .. slayerName .. " is dead!") + qerror("Your slayer, " .. dfhack.df2console(dfhack.units.getReadableName(slayer)) .. " is dead!") end bodyswap.swapAdvUnit(slayer) diff --git a/list-agreements.lua b/list-agreements.lua index 7666c4c39f..55c53a7860 100644 --- a/list-agreements.lua +++ b/list-agreements.lua @@ -66,7 +66,7 @@ end function get_agr_party_name(agr) --assume party 0 is guild/order, 1 is local government as siteid = playerfortid local party_id = agr.parties[0].entity_ids[0] - local party_name = dfhack.TranslateName(df.global.world.entities.all[party_id].name, true) + local party_name = dfhack.translation.translateName(df.global.world.entities.all[party_id].name, true) if not party_name then party_name = 'An Unknown Entity or Group' end @@ -77,7 +77,7 @@ function get_deity_name(agr) local religion_id = agr.details[0].data.Location.deity_data.Religion local deities = df.global.world.entities.all[religion_id].relations.deities if #deities == 0 then return 'An Unknown Deity' end - return dfhack.TranslateName(df.global.world.history.figures[deities[0]].name,true) + return dfhack.translation.translateName(df.global.world.history.figures[deities[0]].name,true) end --get resolution status, and string diff --git a/make-legendary.lua b/make-legendary.lua index ca1edf9529..2098ad0ba2 100644 --- a/make-legendary.lua +++ b/make-legendary.lua @@ -3,7 +3,7 @@ local utils = require('utils') function getName(unit) - return dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) + return dfhack.df2console(dfhack.units.getReadableName(unit)) end function legendize(unit, skill_idx) diff --git a/markdown.lua b/markdown.lua index a914e55bb3..61da34c464 100644 --- a/markdown.lua +++ b/markdown.lua @@ -6,7 +6,7 @@ local gui = require('gui') local argparse = require('argparse') -- Get world name for default filename -local worldName = dfhack.df2utf(dfhack.TranslateName(df.global.world.world_data.name)):gsub(" ", "_") +local worldName = dfhack.df2utf(dfhack.translation.translateName(df.global.world.world_data.name)):gsub(" ", "_") local help, overwrite, filenameArg = false, false, nil local positionals = argparse.processArgsGetopt({ ... }, { diff --git a/modtools/extra-gamelog.lua b/modtools/extra-gamelog.lua index 343b5ef34a..8b9538513c 100644 --- a/modtools/extra-gamelog.lua +++ b/modtools/extra-gamelog.lua @@ -35,7 +35,7 @@ function log_on_load(op) local fort_ent = df.global.plotinfo.main.fortress_entity local civ_ent = df.historical_entity.find(df.global.plotinfo.civ_id) local function fullname(item) - return dfhack.TranslateName(item.name)..' ('..dfhack.TranslateName(item.name ,true)..')' + return dfhack.translation.translateName(item.name)..' ('..dfhack.translation.translateName(item.name ,true)..')' end msg('Loaded '..df.global.world.cur_savegame.save_dir..', '..fullname(df.global.world.world_data).. ' at coordinates ('..site.pos.x..','..site.pos.y..')') @@ -71,7 +71,7 @@ function log_nobles() if expedition_leader == nil then msg("Expedition leader position is now vacant.") else - msg(dfhack.TranslateName(dfhack.units.getVisibleName(expedition_leader)).." became expedition leader.") + msg(dfhack.translation.translateName(dfhack.units.getVisibleName(expedition_leader)).." became expedition leader.") end end @@ -79,7 +79,7 @@ function log_nobles() if mayor == nil then msg("Mayor position is now vacant.") else - msg(dfhack.TranslateName(dfhack.units.getVisibleName(mayor)).." became mayor.") + msg(dfhack.translation.translateName(dfhack.units.getVisibleName(mayor)).." became mayor.") end end old_mayor = mayor diff --git a/modtools/set-belief.lua b/modtools/set-belief.lua index b0b3019218..c46b2adafc 100644 --- a/modtools/set-belief.lua +++ b/modtools/set-belief.lua @@ -263,7 +263,7 @@ end -- Print unit's beliefs into the dfhack console. Cultural beliefs are marked with a * function printUnitBeliefs(unit) - print("Beliefs for " .. dfhack.TranslateName(unit.name) .. ":") + print("Beliefs for " .. dfhack.df2console(dfhack.units.getReadableName(unit)) .. ":") for id, name in ipairs(df.value_type) do if name ~= 'NONE' then local strength = getUnitBelief(unit, id) diff --git a/modtools/set-need.lua b/modtools/set-need.lua index 8698d8744b..fd235e28a7 100644 --- a/modtools/set-need.lua +++ b/modtools/set-need.lua @@ -455,7 +455,7 @@ end -- Print unit's needs, levels, and focus into dfhack console function printUnitNeeds(unit) - print("Needs for " .. dfhack.TranslateName(unit.name) .. ":") + print("Needs for " .. dfhack.df2console(dfhack.units.getReadableName(unit)) .. ":") for index, needInstance in ipairs(unit.status.current_soul.personality.needs) do local name = df.need_type[needInstance.id] local level = needInstance.need_level diff --git a/modtools/set-personality.lua b/modtools/set-personality.lua index 29c966e25c..9b1b421a70 100644 --- a/modtools/set-personality.lua +++ b/modtools/set-personality.lua @@ -243,7 +243,7 @@ function randomTraitValue() end function printUnitTraits(unit) - print("Traits of " .. dfhack.TranslateName(unit.name) .. ":") + print("Traits of " .. dfhack.df2console(dfhack.units.getReadableName(unit)) .. ":") for id, name in ipairs(df.personality_facet_type) do if name ~= 'NONE' then local baseValue = getUnitTraitBase(unit, id) diff --git a/necronomicon.lua b/necronomicon.lua index 47d7c374ac..4914b85a60 100644 --- a/necronomicon.lua +++ b/necronomicon.lua @@ -60,7 +60,7 @@ function necronomicon(include_slabs) for _, item in ipairs(df.global.world.items.other.SLAB) do if check_slab_secrets(item) then local artifact = get_item_artifact(item) - local name = dfhack.TranslateName(artifact.name) + local name = dfhack.translation.translateName(artifact.name) print(" " .. dfhack.df2console(name)) end end @@ -87,7 +87,7 @@ function necronomicon_world(include_slabs) print() for _,rec in ipairs(df.global.world.artifacts.all) do if df.item_slabst:is_instance(rec.item) and check_slab_secrets(rec.item) then - print(dfhack.df2console(dfhack.TranslateName(rec.name))) + print(dfhack.df2console(dfhack.translation.translateName(rec.name))) end end print() diff --git a/pref-adjust.lua b/pref-adjust.lua index a783c97094..067376a008 100644 --- a/pref-adjust.lua +++ b/pref-adjust.lua @@ -236,7 +236,7 @@ function build_all_lists(printflag) list_of_poems_string = "" vec=df.global.world.poetic_forms.all for k=0,#vec-1 do - name=dfhack.TranslateName(vec[k].name,true) + name=dfhack.translation.translateName(vec[k].name,true) list_of_poems[name]=k list_of_poems_string=list_of_poems_string..k..":"..name.."," end @@ -248,7 +248,7 @@ function build_all_lists(printflag) list_of_music_string = "" vec=df.global.world.musical_forms.all for k=0,#vec-1 do - name=dfhack.TranslateName(vec[k].name,true) + name=dfhack.translation.translateName(vec[k].name,true) list_of_music[name]=k list_of_music_string=list_of_music_string..k..":"..name.."," end @@ -260,7 +260,7 @@ function build_all_lists(printflag) list_of_dances_string = "" vec=df.global.world.dance_forms.all for k=0,#vec-1 do - name=dfhack.TranslateName(vec[k].name,true) + name=dfhack.translation.translateName(vec[k].name,true) list_of_dances[name]=k list_of_dances_string=list_of_dances_string..k..":"..name.."," end @@ -270,7 +270,7 @@ function build_all_lists(printflag) end -- end func build_all_lists -- --------------------------------------------------------------------------- function unit_name_to_console(unit) - return dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) + return dfhack.df2console(dfhack.units.getReadableName(unit)) end diff --git a/prefchange.lua b/prefchange.lua index 826d1670bd..81b3f10915 100644 --- a/prefchange.lua +++ b/prefchange.lua @@ -18,7 +18,7 @@ function axeplate() local pss_counter=31415926 local prefcount = #(unit.status.current_soul.preferences) - print ("Before, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("Before, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") -- axes and breastplates utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 4 , item_type = df.item_type.WEAPON , creature_id = df.item_type.WEAPON , color_id = df.item_type.WEAPON , shape_id = df.item_type.WEAPON , plant_id = df.item_type.WEAPON , item_subtype = 1 , mattype = -1 , matindex = -1 , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') @@ -29,7 +29,7 @@ function axeplate() utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 0 , item_type = -1 , creature_id = -1 , color_id = -1 , shape_id = -1 , plant_id = -1 , item_subtype = -1 , mattype = 0 , matindex = dfhack.matinfo.find("STEEL").index , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') prefcount = #(unit.status.current_soul.preferences) - print ("After, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("After, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") end -- --------------------------------------------------------------------------- @@ -44,7 +44,7 @@ function hammershirt() local pss_counter=31415926 local prefcount = #(unit.status.current_soul.preferences) - print ("Before, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("Before, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") -- hammers and mail shirts utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 4 , item_type = df.item_type.WEAPON , creature_id = df.item_type.WEAPON , color_id = df.item_type.WEAPON , shape_id = df.item_type.WEAPON , plant_id = df.item_type.WEAPON , item_subtype = 2 , mattype = -1 , matindex = -1 , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') @@ -55,7 +55,7 @@ function hammershirt() utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 0 , item_type = -1 , creature_id = -1 , color_id = -1 , shape_id = -1 , plant_id = -1 , item_subtype = -1 , mattype = 0 , matindex = dfhack.matinfo.find("STEEL").index , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') prefcount = #(unit.status.current_soul.preferences) - print ("After, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("After, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") end -- --------------------------------------------------------------------------- @@ -70,7 +70,7 @@ function swordboot() local pss_counter=31415926 local prefcount = #(unit.status.current_soul.preferences) - print ("Before, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("Before, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") -- short swords and high boots utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 4 , item_type = df.item_type.WEAPON , creature_id = df.item_type.WEAPON , color_id = df.item_type.WEAPON , shape_id = df.item_type.WEAPON , plant_id = df.item_type.WEAPON , item_subtype = 3 , mattype = -1 , matindex = -1 , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') @@ -81,7 +81,7 @@ function swordboot() utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 0 , item_type = -1 , creature_id = -1 , color_id = -1 , shape_id = -1 , plant_id = -1 , item_subtype = -1 , mattype = 0 , matindex = dfhack.matinfo.find("STEEL").index , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') prefcount = #(unit.status.current_soul.preferences) - print ("After, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("After, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") end -- --------------------------------------------------------------------------- @@ -96,7 +96,7 @@ function spearboot() local pss_counter=31415926 local prefcount = #(unit.status.current_soul.preferences) - print ("Before, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("Before, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") -- spears and high boots utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 4 , item_type = df.item_type.WEAPON , creature_id = df.item_type.WEAPON , color_id = df.item_type.WEAPON , shape_id = df.item_type.WEAPON , plant_id = df.item_type.WEAPON , item_subtype = 4 , mattype = -1 , matindex = -1 , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') @@ -107,7 +107,7 @@ function spearboot() utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 0 , item_type = -1 , creature_id = -1 , color_id = -1 , shape_id = -1 , plant_id = -1 , item_subtype = -1 , mattype = 0 , matindex = dfhack.matinfo.find("STEEL").index , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') prefcount = #(unit.status.current_soul.preferences) - print ("After, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("After, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") end -- --------------------------------------------------------------------------- @@ -122,7 +122,7 @@ function maceshield() local pss_counter=31415926 local prefcount = #(unit.status.current_soul.preferences) - print ("Before, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("Before, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") -- maces and shields utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 4 , item_type = df.item_type.WEAPON , creature_id = df.item_type.WEAPON , color_id = df.item_type.WEAPON , shape_id = df.item_type.WEAPON , plant_id = df.item_type.WEAPON , item_subtype = 5 , mattype = -1 , matindex = -1 , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') @@ -133,7 +133,7 @@ function maceshield() utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 0 , item_type = -1 , creature_id = -1 , color_id = -1 , shape_id = -1 , plant_id = -1 , item_subtype = -1 , mattype = 0 , matindex = dfhack.matinfo.find("STEEL").index , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') prefcount = #(unit.status.current_soul.preferences) - print ("After, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("After, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") end -- --------------------------------------------------------------------------- @@ -148,7 +148,7 @@ function xbowhelm() local pss_counter=31415926 local prefcount = #(unit.status.current_soul.preferences) - print ("Before, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("Before, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") -- crossbows and helms utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 4 , item_type = df.item_type.WEAPON , creature_id = df.item_type.WEAPON , color_id = df.item_type.WEAPON , shape_id = df.item_type.WEAPON , plant_id = df.item_type.WEAPON , item_subtype = 6 , mattype = -1 , matindex = -1 , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') @@ -159,7 +159,7 @@ function xbowhelm() utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 0 , item_type = -1 , creature_id = -1 , color_id = -1 , shape_id = -1 , plant_id = -1 , item_subtype = -1 , mattype = 0 , matindex = dfhack.matinfo.find("STEEL").index , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') prefcount = #(unit.status.current_soul.preferences) - print ("After, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("After, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") end -- --------------------------------------------------------------------------- @@ -174,7 +174,7 @@ function pickglove() local pss_counter=31415926 local prefcount = #(unit.status.current_soul.preferences) - print ("Before, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("Before, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") -- picks and gauntlets utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 4 , item_type = df.item_type.WEAPON , creature_id = df.item_type.WEAPON , color_id = df.item_type.WEAPON , shape_id = df.item_type.WEAPON , plant_id = df.item_type.WEAPON , item_subtype = 7 , mattype = -1 , matindex = -1 , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') @@ -185,7 +185,7 @@ function pickglove() utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 0 , item_type = -1 , creature_id = -1 , color_id = -1 , shape_id = -1 , plant_id = -1 , item_subtype = -1 , mattype = 0 , matindex = dfhack.matinfo.find("STEEL").index , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') prefcount = #(unit.status.current_soul.preferences) - print ("After, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("After, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") end -- --------------------------------------------------------------------------- @@ -200,7 +200,7 @@ function longglove() local pss_counter=31415926 local prefcount = #(unit.status.current_soul.preferences) - print ("Before, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("Before, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") -- long swords and gauntlets, skipped bows and whatnot utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 4 , item_type = df.item_type.WEAPON , creature_id = df.item_type.WEAPON , color_id = df.item_type.WEAPON , shape_id = df.item_type.WEAPON , plant_id = df.item_type.WEAPON , item_subtype = 13 , mattype = -1 , matindex = -1 , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') @@ -211,7 +211,7 @@ function longglove() utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 0 , item_type = -1 , creature_id = -1 , color_id = -1 , shape_id = -1 , plant_id = -1 , item_subtype = -1 , mattype = 0 , matindex = dfhack.matinfo.find("STEEL").index , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') prefcount = #(unit.status.current_soul.preferences) - print ("After, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("After, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") end -- --------------------------------------------------------------------------- @@ -226,7 +226,7 @@ function daggerpants() local pss_counter=31415926 local prefcount = #(unit.status.current_soul.preferences) - print ("Before, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("Before, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") -- daggers and greaves, skipped the weapons which are too large for most dwarves utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 4 , item_type = df.item_type.WEAPON , creature_id = df.item_type.WEAPON , color_id = df.item_type.WEAPON , shape_id = df.item_type.WEAPON , plant_id = df.item_type.WEAPON , item_subtype = 16 , mattype = -1 , matindex = -1 , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') @@ -237,7 +237,7 @@ function daggerpants() utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = 0 , item_type = -1 , creature_id = -1 , color_id = -1 , shape_id = -1 , plant_id = -1 , item_subtype = -1 , mattype = 0 , matindex = dfhack.matinfo.find("STEEL").index , active = true, prefstring_seed = pss_counter }, 'prefstring_seed') prefcount = #(unit.status.current_soul.preferences) - print ("After, unit "..dfhack.TranslateName(dfhack.units.getVisibleName(unit)).." has "..prefcount.." preferences") + print ("After, unit "..dfhack.df2console(dfhack.units.getReadableName(unit)).." has "..prefcount.." preferences") end -- --------------------------------------------------------------------------- @@ -274,16 +274,16 @@ function clear_all(v) end -- --------------------------------------------------------------------------- function printpref_all_dwarves() - for _,v in ipairs(dfhack.units.getCitizens()) do - print("Showing Preferences for "..dfhack.TranslateName(dfhack.units.getVisibleName(v))) - print_all(v) + for _,unit in ipairs(dfhack.units.getCitizens()) do + print("Showing Preferences for "..dfhack.df2console(dfhack.units.getReadableName(unit))) + print_all(unit) end end -- --------------------------------------------------------------------------- function clearpref_all_dwarves() - for _,v in ipairs(dfhack.units.getCitizens()) do - print("Clearing Preferences for "..dfhack.TranslateName(dfhack.units.getVisibleName(v))) - clear_all(v) + for _,unit in ipairs(dfhack.units.getCitizens()) do + print("Clearing Preferences for "..dfhack.df2console(dfhack.units.getReadableName(unit))) + clear_all(unit) end end -- --------------------------------------------------------------------------- diff --git a/set-orientation.lua b/set-orientation.lua index 6b821b8fcf..f1b1233cd5 100644 --- a/set-orientation.lua +++ b/set-orientation.lua @@ -214,7 +214,7 @@ function main(...) -- View if args.view then - print("Orientation of " .. dfhack.TranslateName(unit.name) .. ":") + print("Orientation of " .. dfhack.df2console(dfhack.units.getReadableName(unit)) .. ":") print("Male: " .. getInterestString(getInterest(unit, "male"))) print("Female: " .. getInterestString(getInterest(unit, "female"))) return diff --git a/superdwarf.lua b/superdwarf.lua index 16f3cacffb..d3dd754ce0 100644 --- a/superdwarf.lua +++ b/superdwarf.lua @@ -21,12 +21,7 @@ local function getUnit(obj) end local function getName(unit) - local name = dfhack.TranslateName(unit.name) - -- Animals will have a profession name if they aren't named - if name == '' then - name = dfhack.units.getProfessionName(unit) - end - return name + return dfhack.df2console(dfhack.units.getReadableName(unit)) end local function onTimer() diff --git a/troubleshoot-item.lua b/troubleshoot-item.lua index a509407048..db7167b558 100644 --- a/troubleshoot-item.lua +++ b/troubleshoot-item.lua @@ -1,13 +1,4 @@ --- troubleshoot-item.lua --@ module = true ---[====[ - -troubleshoot-item -================= -Print various properties of the selected item. Sometimes useful for -troubleshooting issues such as why dwarves won't pick up a certain item. - -]====] local function coord_to_str(coord) local out = {} @@ -45,8 +36,7 @@ function troubleshoot_item(item, out) if unit_holder.unit_id then local unit = df.unit.find(unit_holder.unit_id) if unit then - local unit_details = string.format("%s - %s", dfhack.TranslateName(dfhack.units.getVisibleName(unit)), dfhack.units.getProfessionName(unit)) - out('Held by unit: ' .. unit_details) + out('Held by unit: ' .. dfhack.units.getReadableName(unit)) else warn('Could not find unit with unit_id: ' .. unit_holder.unit_id) end diff --git a/uniform-unstick.lua b/uniform-unstick.lua index e77663242e..be3a4ad858 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -106,7 +106,7 @@ end -- Will figure out which items need to be moved to the floor, returns an item_id:item map local function process(unit, args, need_newline) local silent = args.all -- Don't print details if we're iterating through all dwarves - local unit_name = dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) + local unit_name = dfhack.df2console(dfhack.units.getReadableName(unit)) if not silent then need_newline = print_line("Processing unit " .. unit_name, need_newline) diff --git a/unretire-anyone.lua b/unretire-anyone.lua index 0e79fccaba..be2a50c1a0 100644 --- a/unretire-anyone.lua +++ b/unretire-anyone.lua @@ -70,8 +70,8 @@ function showNemesisPrompt(advSetUpScreen) end if histFig.name.has_name then name = name .. - '\n' .. dfhack.TranslateName(histFig.name) .. - '\n"' .. dfhack.TranslateName(histFig.name, true) .. '"' + '\n' .. dfhack.translation.translateName(histFig.name) .. + '\n"' .. dfhack.translation.translateName(histFig.name, true) .. '"' else name = name .. '\nUnnamed' diff --git a/warn-stranded.lua b/warn-stranded.lua index 239c4b335d..d916057d05 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -132,7 +132,7 @@ end local function getUnitDescription(unit) return ('[%s] %s %s'):format( dfhack.units.getProfessionName(unit), - dfhack.TranslateName(dfhack.units.getVisibleName(unit)), + dfhack.translation.translateName(dfhack.units.getVisibleName(unit)), getSexString(unit.sex)) end From b1fcb1940357c201a93b7a625af577a001ccd16a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 Jan 2025 03:55:27 -0800 Subject: [PATCH 305/811] add temporary workaround for widgets::widget --- devel/scan-vtables.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/devel/scan-vtables.lua b/devel/scan-vtables.lua index 74ce5a1294..1b62ec424b 100644 --- a/devel/scan-vtables.lua +++ b/devel/scan-vtables.lua @@ -71,7 +71,8 @@ function scan_ranges(g_src) if demangled_name and not demangled_name:match('[<>]') and not demangled_name:match('^std::') and - not names[demangled_name] + not names[demangled_name] and + (g_src or demangled_name ~= 'widgets::widget') -- the widget in g_src takes precedence then local base_str = '' if g_src then From 8d345a743557216e7c5493e000f0b9d7a670d3d8 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 12 Jan 2025 13:03:05 +0100 Subject: [PATCH 306/811] make this an overlay --- gui/tooltips.lua | 75 ++++++++++++++++++++++++++++++++++-------------- 1 file changed, 54 insertions(+), 21 deletions(-) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index 671b8faf3b..d9015afff0 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -1,20 +1,25 @@ -- Show tooltips on units and/or mouse +--@ module = true + local RELOAD = false -- set to true when actively working on this script local gui = require('gui') local widgets = require('gui.widgets') +local overlay = require('plugins.overlay') local ResizingPanel = require('gui.widgets.containers.resizing_panel') -------------------------------------------------------------------------------- -local follow_units = true; -local follow_mouse = true; +config = config or { + follow_units = true, + follow_mouse = true, +} local function change_follow_units(new, old) - follow_units = new + config.follow_units = new end local function change_follow_mouse(new, old) - follow_mouse = new + config.follow_mouse = new end local shortenings = { @@ -25,6 +30,22 @@ local shortenings = { local TITLE = "Tooltips" +if RELOAD then TooltipControlScreen = nil end +TooltipControlScreen = defclass(TooltipControlScreen, gui.ZScreen) +TooltipControlScreen.ATTRS { + focus_path = "TooltipControlScreen", + pass_movement_keys = true, +} + +function TooltipControlScreen:init() + local controls = TooltipControlWindow{view_id = 'controls'} + self:addviews{controls} +end + +function TooltipControlScreen:onDismiss() + view = nil +end + if RELOAD then TooltipControlWindow = nil end TooltipControlWindow = defclass(TooltipControlWindow, widgets.Window) TooltipControlWindow.ATTRS { @@ -130,7 +151,7 @@ function MouseTooltip:init() end function MouseTooltip:render(dc) - if not follow_mouse then return end + if not config.follow_mouse then return end local x, y = dfhack.screen.getMousePos() if not x then return end @@ -149,22 +170,25 @@ function MouseTooltip:render(dc) end -------------------------------------------------------------------------------- - -if RELOAD then TooltipsVizualizer = nil end -TooltipsVizualizer = defclass(TooltipsVizualizer, gui.ZScreen) -TooltipsVizualizer.ATTRS{ - focus_path='TooltipsVizualizer', - pass_movement_keys=true, +if RELOAD then TooltipsOverlay = nil end +TooltipsOverlay = defclass(TooltipsOverlay, overlay.OverlayWidget) +TooltipsOverlay.ATTRS{ + desc='Adds tooltips with some info to units.', + default_pos={x=1,y=1}, + default_enabled=true, + fullscreen=true, -- not player-repositionable + viewscreens={ + 'dwarfmode/Default', + }, } -function TooltipsVizualizer:init() - local controls = TooltipControlWindow{view_id = 'controls'} +function TooltipsOverlay:init() local tooltip = MouseTooltip{view_id = 'tooltip'} - self:addviews{controls, tooltip} + self:addviews{tooltip} end -- map coordinates -> interface layer coordinates -function GetScreenCoordinates(map_coord) +local function GetScreenCoordinates(map_coord) if not map_coord then return end -- -> map viewport offset local vp = df.global.world.viewport @@ -195,10 +219,10 @@ function GetScreenCoordinates(map_coord) end end -function TooltipsVizualizer:onRenderFrame(dc, rect) - TooltipsVizualizer.super.onRenderFrame(self, dc, rect) +function TooltipsOverlay:render(dc) + TooltipsOverlay.super.render(self, dc) - if not follow_units then return end + if not config.follow_units then return end if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then return @@ -282,12 +306,21 @@ function TooltipsVizualizer:onRenderFrame(dc, rect) end end -function TooltipsVizualizer:onDismiss() - view = nil +function TooltipsOverlay:preUpdateLayout(parent_rect) + self.frame.w = parent_rect.width + self.frame.h = parent_rect.height end ---------------------------------------------------------------- +OVERLAY_WIDGETS = { + tooltips=TooltipsOverlay, +} + +if dfhack_flags.module then + return +end + if not dfhack.isMapLoaded() then qerror('gui/tooltips requires a map to be loaded') end @@ -297,4 +330,4 @@ if RELOAD and view then -- view is nil now end -view = view and view:raise() or TooltipsVizualizer{}:show() +view = view and view:raise() or TooltipControlScreen{}:show() From 9992589495582c82a28ef15685f782b24ac1d8e4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 12 Jan 2025 06:26:49 -0800 Subject: [PATCH 307/811] refactor for handling word selectors, add word selector selector --- gui/rename.lua | 300 ++++++++++++++++++++++++++++++++----------------- 1 file changed, 194 insertions(+), 106 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 8f4cfe3c6d..907a5270a5 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -20,27 +20,68 @@ local translations = df.language_translation.get_vector() -- target selection -- +local wt = language.word_table + +local function get_word_selectors(name_type, civ) + -- default to something generic with a lot of word choices + local major, minor = wt[0][df.language_name_category.River], wt[1][df.language_name_category.River] + + -- TODO: locations + + -- refine word selector choice based on context + if name_type == df.language_name_type.Figure then + if civ then + major, minor = civ.entity_raw.symbols.symbols_major.OTHER, civ.entity_raw.symbols.symbols_minor.OTHER + else + major, minor = wt[0][df.language_name_category.Unit], wt[1][df.language_name_category.Unit] + end + elseif name_type == df.language_name_type.World then + major, minor = wt[0][df.language_name_category.Region], wt[1][df.language_name_category.Region] + elseif name_type == df.language_name_type.Artifact then + -- The game normally only uses ArtifactEvil if it was created by a fell/macabre mood, but we don't know + -- at this point, so we'll randomize the choice + if math.random(5) == 1 then + major, minor = wt[0][df.language_name_category.ArtifactEvil], wt[1][df.language_name_category.ArtifactEvil] + else + major, minor = wt[0][df.language_name_category.Artifact], wt[1][df.language_name_category.Artifact] + end + elseif name_type == df.language_name_type.Civilization and civ then + major, minor = civ.entity_raw.symbols.symbols_major.CIV, civ.entity_raw.symbols.symbols_minor.CIV + elseif name_type == df.language_name_type.EntitySite and civ then + major, minor = civ.entity_raw.symbols.symbols_major.SITE, civ.entity_raw.symbols.symbols_minor.SITE + elseif name_type == df.language_name_type.Site and civ then + major, minor = civ.entity_raw.symbols.symbols_major.OTHER, civ.entity_raw.symbols.symbols_minor.OTHER + elseif name_type == df.language_name_type.Squad and civ then + major, minor = civ.entity_raw.symbols.symbols_major.OTHER, civ.entity_raw.symbols.symbols_minor.OTHER + end + + return major, minor +end + local function get_artifact_target(item) if not item or not item.flags.artifact then return end local gref = dfhack.items.getGeneralRef(item, df.general_ref_type.IS_ARTIFACT) if not gref then return end local rec = df.artifact_record.find(gref.artifact_id) if not rec then return end - return rec.name + local major_selector, minor_selector = get_word_selectors(df.language_name_type.Artifact) + return {name=rec.name, major_selector=major_selector, minor_selector=minor_selector} end local function get_hf_target(hf) if not hf then return end - local target = dfhack.units.getVisibleName(hf) + local name = dfhack.units.getVisibleName(hf) local unit = df.unit.find(hf.unit_id) - local sync_targets = {} + local sync_names = {} if unit then local unit_name = dfhack.units.getVisibleName(unit) - if unit_name ~= target then - table.insert(sync_targets, unit_name) + if unit_name ~= name then + table.insert(sync_names, unit_name) end end - return target, sync_targets + local civ = df.historical_entity.find(hf.civ_id) + local major_selector, minor_selector = get_word_selectors(df.language_name_type.Figure, civ) + return {name=name, sync_names=sync_names, major_selector=major_selector, minor_selector=minor_selector} end local function get_unit_target(unit) @@ -50,26 +91,72 @@ local function get_unit_target(unit) return get_hf_target(hf) end -- unit with no hf - return dfhack.units.getVisibleName(unit), {} + local civ = df.historical_entity.find(unit.civ_id) + local major_selector, minor_selector = get_word_selectors(df.language_name_type.Figure, civ) + return {name=dfhack.units.getVisibleName(unit), major_selector=major_selector, minor_selector=minor_selector} +end + +local function get_civ_from_entity(entity) + if not entity then return end + if entity.type == df.historical_entity_type.Civilization then return entity end + for _,ee_link in ipairs(entity.entity_links) do + if ee_link.type ~= df.entity_entity_link_type.PARENT then goto continue end + local linked_he = df.historical_entity.find(ee_link.target) + if linked_he and linked_he.type == df.historical_entity_type.Civilization then + return linked_he + end + ::continue:: + end +end + +local function get_civ_from_site(site) + if not site then return end + for _,he_link in ipairs(site.entity_links) do + if he_link.type ~= df.entity_site_link_type.All then goto continue end + local linked_he = df.historical_entity.find(he_link.entity_id) + if linked_he and linked_he.type == df.historical_entity_type.Civilization then + return linked_he + end + ::continue:: + end +end + +local function get_entity_target(entity) + if not entity then return end + local major_selector, minor_selector = get_word_selectors(entity.name.type, get_civ_from_entity(entity)) + return {name=entity.name, major_selector=major_selector, minor_selector=minor_selector} +end + +local function get_site_target(site) + if not site then return end + local major_selector, minor_selector = get_word_selectors(site.name.type, get_civ_from_site(site)) + return {name=site.name, major_selector=major_selector, minor_selector=minor_selector} end local function get_location_target(site, loc_id) if not site or loc_id < 0 then return end local loc = utils.binsearch(site.buildings, loc_id, 'id') if not loc then return end - return loc.name + local major_selector, minor_selector = get_word_selectors(loc.name.type, get_civ_from_site(site)) + return {name=loc.name, major_selector=major_selector, minor_selector=minor_selector} +end + +local function get_squad_target(fort, squad) + local major_selector, minor_selector = get_word_selectors(squad.name.type, get_civ_from_entity(fort)) + return {name=squad.name, major_selector=major_selector, minor_selector=minor_selector} end local function get_world_target() - local target = df.global.world.world_data.name - local sync_targets = { + local name = df.global.world.world_data.name + local sync_names = { function() df.global.world.cur_savegame.world_header.world_name = - ('%s, "%s"'):format(dfhack.translation.translateName(target), - dfhack.translation.translateName(target, true)) - end - } - return target, sync_targets + ('%s, "%s"'):format(dfhack.translation.translateName(name), + dfhack.translation.translateName(name, true)) + end + } + local major_selector, minor_selector = get_word_selectors(df.language_name_type.World) + return {name=name, sync_names=sync_names, major_selector=major_selector, minor_selector=minor_selector} end local function select_artifact(cb) @@ -99,15 +186,19 @@ local function select_location(site, cb) {text=desc, pen=pen}, ')', }, - data={target=loc.name}, + data={target=get_location_target(site, loc.id)}, }) end dlg.showListPrompt('Rename', 'Select a location to rename:', COLOR_WHITE, choices, function(_, choice) cb(choice.data.target) end, nil, nil, true) end -local function select_entity(site, cb) - cb(site.name) +local function select_entity(entity, cb) + cb(get_entity_target(entity)) +end + +local function select_site(site, cb) + cb(get_site_target(site)) end local function select_squad(fort, cb) @@ -117,7 +208,7 @@ local function select_squad(fort, cb) if squad then table.insert(choices, { text=dfhack.military.getSquadName(squad.id), - data={target=squad.name}, + data={target=get_squad_target(fort, squad)}, }) end end @@ -130,22 +221,21 @@ local function select_unit(cb) -- scan through units.all instead of units.active so we can choose starting dwarves on embark prep screen for _,unit in ipairs(df.global.world.units.all) do if not dfhack.units.isActive(unit) then goto continue end - local target, sync_targets = get_unit_target(unit) + local target = get_unit_target(unit) if not target then goto continue end table.insert(choices, { text=dfhack.units.getReadableName(unit), - data={target=target, sync_targets=sync_targets}, + data={target=target}, }) ::continue:: end dlg.showListPrompt('Rename', 'Select a unit to rename:', COLOR_WHITE, - choices, function(_, choice) cb(choice.data.target, choice.data.sync_targets) end, + choices, function(_, choice) cb(choice.data.target) end, nil, nil, true) end local function select_world(cb) - local target, sync_targets = get_world_target() - cb(target, sync_targets) + cb(get_world_target()) end local function select_new_target(cb) @@ -161,7 +251,7 @@ local function select_new_target(cb) if #site.buildings > 0 then table.insert(choices, {text='A location', data={fn=curry(select_location, site)}}) end - table.insert(choices, {text='This fortress/site', data={fn=curry(select_entity, site)}}) + table.insert(choices, {text='This fortress/site', data={fn=curry(select_site, site)}}) if fort and #fort.squads > 0 then table.insert(choices, {text='A squad', data={fn=curry(select_squad, fort)}}) end @@ -273,13 +363,13 @@ end function Rename:init(info) self.target = info.target - self.sync_targets = info.sync_targets or {} self.cache = {} local function normalize_name() - if self.target.type == df.language_name_type.NONE then - self.target.type = df.language_name_type.Figure + if self.target.name.type == df.language_name_type.NONE then + self.target.name.type = df.language_name_type.Figure end + self.target.sync_names = self.target.sync_names or {} end normalize_name() @@ -294,30 +384,30 @@ function Rename:init(info) label='Select new target', auto_width=true, on_activate=function() - select_new_target(function(target, sync_targets) + select_new_target(function(target) if not target then return end - self.target, self.sync_targets = target, sync_targets or {} + self.target = target normalize_name() - self.subviews.language:setOption(self.target.language) + self.subviews.language:setOption(self.target.name.language) self:refresh_list() end) end, visible=info.show_selector, }, - -- widgets.HotkeyLabel{ - -- frame={t=0, r=0}, - -- key='CUSTOM_CTRL_G', - -- label='Generate random name', - -- auto_width=true, - -- on_activate=self:callback('generate_random_name'), - -- }, + widgets.HotkeyLabel{ + frame={t=0, r=0}, + key='CUSTOM_CTRL_G', + label='Generate random name', + auto_width=true, + on_activate=self:callback('generate_random_name'), + }, widgets.Label{ frame={t=2}, - text={{pen=COLOR_YELLOW, text=function() return pad_text(dfhack.translation.translateName(self.target), self.frame_body.width) end}}, + text={{pen=COLOR_YELLOW, text=function() return pad_text(dfhack.translation.translateName(self.target.name), self.frame_body.width) end}}, }, widgets.Label{ frame={t=3}, - text={{pen=COLOR_LIGHTCYAN, text=function() return pad_text(('"%s"'):format(dfhack.translation.translateName(self.target, true)), self.frame_body.width) end}}, + text={{pen=COLOR_LIGHTCYAN, text=function() return pad_text(('"%s"'):format(dfhack.translation.translateName(self.target.name, true)), self.frame_body.width) end}}, }, widgets.CycleHotkeyLabel{ view_id='language', @@ -325,12 +415,12 @@ function Rename:init(info) key='CUSTOM_CTRL_T', label='Language:', options=language_options, - initial_option=self.target and self.target.language or 0, + initial_option=self.target.name.language, on_change=self:callback('set_language'), }, widgets.Label{ frame={t=6, l=7}, - text={'Name type: ', {pen=COLOR_CYAN, text=function() return df.language_name_type[self.target.type] end}}, + text={'Name type: ', {pen=COLOR_CYAN, text=function() return df.language_name_type[self.target.name.type] end}}, }, }, }, @@ -369,7 +459,7 @@ function Rename:init(info) label='Prev component', on_activate=function() local clist = self.subviews.component_list - local move = self.target.type ~= df.language_name_type.Figure and + local move = self.target.name.type ~= df.language_name_type.Figure and clist:getSelected() == 2 and #clist:getChoices()-2 or -1 self.subviews.component_list:moveCursor(move) end, @@ -381,7 +471,7 @@ function Rename:init(info) label='Next component', on_activate=function() local clist = self.subviews.component_list - local move = self.target.type ~= df.language_name_type.Figure and + local move = self.target.name.type ~= df.language_name_type.Figure and clist:getSelected() == #clist:getChoices() and -#clist:getChoices()+2 or 1 self.subviews.component_list:moveCursor(move) end, @@ -410,7 +500,7 @@ function Rename:init(info) enabled=function() local _, comp_choice = self.subviews.component_list:getSelected() if comp_choice.data.is_first_name then return false end - return self.target.words[comp_choice.data.val] >= 0 + return self.target.name.words[comp_choice.data.val] >= 0 end, }, }, @@ -499,18 +589,18 @@ function Rename:get_component_choices() table.insert(choices, { text={ {text='First Name', - pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or nil end}, + pen=function() return self.target.name.type ~= df.language_name_type.Figure and COLOR_GRAY or nil end}, NEWLINE, - {gap=2, pen=COLOR_YELLOW, text=function() return self.target.first_name end} + {gap=2, pen=COLOR_YELLOW, text=function() return self.target.name.first_name end} }, data={val=df.language_name_component.TheX, is_first_name=true}}) for val, comp in ipairs(df.language_name_component) do local text = { {text=comp:gsub('(%l)(%u)', '%1 %2')}, NEWLINE, {gap=2, pen=COLOR_YELLOW, text=function() - local word = self.target.words[val] + local word = self.target.name.words[val] if word < 0 then return end - return ('%s'):format(language.words[word].forms[self.target.parts_of_speech[val]]) + return ('%s'):format(language.words[word].forms[self.target.name.parts_of_speech[val]]) end}, } table.insert(choices, {text=text, data={val=val}}) @@ -522,9 +612,9 @@ function Rename:get_component_action_choices() local choices = {} table.insert(choices, { text={ - {text='[', pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or COLOR_RED end}, - {text='Random', pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or nil end}, - {text=']', pen=function() return self.target.type ~= df.language_name_type.Figure and COLOR_GRAY or COLOR_RED end} + {text='[', pen=function() return self.target.name.type ~= df.language_name_type.Figure and COLOR_GRAY or COLOR_RED end}, + {text='Random', pen=function() return self.target.name.type ~= df.language_name_type.Figure and COLOR_GRAY or nil end}, + {text=']', pen=function() return self.target.name.type ~= df.language_name_type.Figure and COLOR_GRAY or COLOR_RED end} }, data={fn=self:callback('randomize_first_name')}, }) @@ -536,9 +626,9 @@ function Rename:get_component_action_choices() local randomize_fn = self:callback('randomize_component_word', comp) table.insert(choices, {text=randomize_text, data={fn=randomize_fn}}) local clear_text = { - {text=function() return self.target.words[comp] >= 0 and '[' or '' end, pen=COLOR_RED}, - {text=function() return self.target.words[comp] >= 0 and 'Clear' or '' end }, - {text=function() return self.target.words[comp] >= 0 and ']' or '' end, pen=COLOR_RED} + {text=function() return self.target.name.words[comp] >= 0 and '[' or '' end, pen=COLOR_RED}, + {text=function() return self.target.name.words[comp] >= 0 and 'Clear' or '' end }, + {text=function() return self.target.name.words[comp] >= 0 and ']' or '' end, pen=COLOR_RED} } local clear_fn = self:callback('clear_component_word', comp) table.insert(choices, {text=clear_text, data={fn=clear_fn}}) @@ -548,39 +638,39 @@ function Rename:get_component_action_choices() end function Rename:clear_component_word(comp) - self.target.words[comp] = -1 - for _, sync_target in ipairs(self.sync_targets) do - if type(sync_target) == 'function' then - sync_target() + self.target.name.words[comp] = -1 + for _, sync_name in ipairs(self.target.sync_names) do + if type(sync_name) == 'function' then + sync_name() else - sync_target.words[comp] = -1 + sync_name.words[comp] = -1 end end end function Rename:set_first_name(word_idx) -- support giving names to previously unnamed units - self.target.has_name = true + self.target.name.has_name = true - self.target.first_name = translations[self.subviews.language:getOptionValue()].words[word_idx].value - for _, sync_target in ipairs(self.sync_targets) do - if type(sync_target) == 'function' then - sync_target() + self.target.name.first_name = translations[self.subviews.language:getOptionValue()].words[word_idx].value + for _, sync_name in ipairs(self.target.sync_names) do + if type(sync_name) == 'function' then + sync_name() else - sync_target.first_name = self.target.first_name + sync_name.first_name = self.target.name.first_name end end end function Rename:set_component_word_by_data(component, word_idx, part_of_speech) - self.target.words[component] = word_idx - self.target.parts_of_speech[component] = part_of_speech - for _, sync_target in ipairs(self.sync_targets) do - if type(sync_target) == 'function' then - sync_target() + self.target.name.words[component] = word_idx + self.target.name.parts_of_speech[component] = part_of_speech + for _, sync_name in ipairs(self.target.sync_names) do + if type(sync_name) == 'function' then + sync_name() else - sync_target.words[component] = word_idx - sync_target.parts_of_speech[component] = part_of_speech + sync_name.words[component] = word_idx + sync_name.parts_of_speech[component] = part_of_speech end end end @@ -595,22 +685,22 @@ function Rename:set_component_word(_, choice) end function Rename:set_language(val, prev_val) - self.target.language = val + self.target.name.language = val -- translate current first name into target language - local idx = utils.linear_index(translations[prev_val].words, self.target.first_name, 'value') - if idx then self.target.first_name = translations[val].words[idx].value end - for _, sync_target in ipairs(self.sync_targets) do - if type(sync_target) == 'function' then - sync_target() + local idx = utils.linear_index(translations[prev_val].words, self.target.name.first_name, 'value') + if idx then self.target.name.first_name = translations[val].words[idx].value end + for _, sync_name in ipairs(self.target.sync_names) do + if type(sync_name) == 'function' then + sync_name() else - sync_target.language = val - sync_target.first_name = self.target.first_name + sync_name.language = val + sync_name.first_name = self.target.first_name end end end function Rename:randomize_first_name() - if self.target.type ~= df.language_name_type.Figure then return end + if self.target.name.type ~= df.language_name_type.Figure then return end local choices = self:get_word_choices(df.language_name_component.TheX) self:set_first_name(choices[math.random(#choices)].data.idx) end @@ -622,15 +712,15 @@ function Rename:randomize_component_word(comp) end function Rename:generate_random_name() - print('TODO: call dfhack.translation.generateName API once it exists') - -- dfhack.translation.generateName(self.target) - -- for _, sync_target in ipairs(self.sync_targets) do - -- if type(sync_target) == 'function' then - -- sync_target() - -- else - -- df.assign(sync_target, self.target) - -- end - -- end + dfhack.translation.generateName(self.target.name, self.target.name.language, self.target.name.type, + self.target.major_selector, self.target.minor_selector) + for _, sync_name in ipairs(self.target.sync_names) do + if type(sync_name) == 'function' then + sync_name() + else + df.assign(sync_name, self.target.name) + end + end end local part_of_speech_to_display = { @@ -656,9 +746,9 @@ function Rename:add_word_choice(choices, comp, idx, word, part_of_speech) local function get_pen() local _, comp_choice = clist:getSelected() if comp_choice.data.is_first_name then - return get_native() == self.target.first_name and COLOR_YELLOW or nil + return get_native() == self.target.name.first_name and COLOR_YELLOW or nil end - if idx == self.target.words[comp] and part_of_speech == self.target.parts_of_speech[comp] then + if idx == self.target.name.words[comp] and part_of_speech == self.target.name.parts_of_speech[comp] then return COLOR_YELLOW end end @@ -726,7 +816,7 @@ end function Rename:refresh_list(sort_widget, sort_fn) local clist = self.subviews.component_list if not clist then return end - if self.target.type ~= df.language_name_type.Figure and clist:getSelected() == 1 then + if self.target.name.type ~= df.language_name_type.Figure and clist:getSelected() == 1 then clist:setSelected(self.prev_selected_component ~= 1 and self.prev_selected_component or 2) end self.prev_selected_component = clist:getSelected() @@ -763,7 +853,6 @@ function RenameScreen:init(info) self:addviews{ Rename{ target=info.target, - sync_targets=info.sync_targets or {}, show_selector=info.show_selector, } } @@ -875,9 +964,9 @@ if not dfhack.isWorldLoaded() then end local function get_target(opts) - local target, sync_targets = nil, {} + local target if opts.histfig_id then - target, sync_targets = get_hf_target(df.historical_figure.find(opts.histfig_id)) + target = get_hf_target(df.historical_figure.find(opts.histfig_id)) if not target then qerror('Historical figure not found') end elseif opts.item_id then target = get_artifact_target(df.item.find(opts.item_id)) @@ -896,12 +985,12 @@ local function get_target(opts) if not squad then qerror('Squad not found') end target = squad.name elseif opts.unit_id then - target, sync_targets = get_unit_target(df.unit.find(opts.unit_id)) + target = get_unit_target(df.unit.find(opts.unit_id)) if not target then qerror('Unit not found') end elseif opts.world then - target, sync_targets = get_world_target() + target = get_world_target() end - return target, sync_targets + return target end local function main(args) @@ -935,17 +1024,16 @@ local function main(args) return end - local function launch(target, sync_targets) + local function launch(target) view = view and view:raise() or RenameScreen{ target=target, - sync_targets=sync_targets, show_selector=opts.show_selector, }:show() end - local target, sync_targets = get_target(opts) + local target = get_target(opts) if target then - launch(target, sync_targets) + launch(target) return end @@ -953,14 +1041,14 @@ local function main(args) local item = dfhack.gui.getSelectedItem(true) local zone = dfhack.gui.getSelectedCivZone(true) if unit then - target, sync_targets = get_unit_target(unit) + target = get_unit_target(unit) elseif item then target = get_artifact_target(item) elseif zone then target = get_location_target(df.world_site.find(zone.site_id), zone.location_id) end if target then - launch(target, sync_targets) + launch(target) return end From 6aabbebbed0460476272fca52fb4c752db690ae7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 12 Jan 2025 18:42:10 +0100 Subject: [PATCH 308/811] Fix notes tool to use new TextArea instead of deprecated TextEditor --- notes.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/notes.lua b/notes.lua index 99aab4a748..9bb4bc7bce 100644 --- a/notes.lua +++ b/notes.lua @@ -5,7 +5,6 @@ local widgets = require('gui.widgets') local textures = require('gui.textures') local overlay = require('plugins.overlay') local guidm = require('gui.dwarfmode') -local text_editor = reqscript('internal/journal/text_editor') local green_pin = dfhack.textures.loadTileset( 'hack/data/art/note_green_pin_map.png', @@ -172,7 +171,7 @@ function NoteManager:init() auto_width=true, on_activate=function() self.subviews.name:setFocus(true) end, }, - text_editor.TextEditor{ + widgets.TextArea{ view_id='name', frame={t=1,h=3}, frame_style=gui.FRAME_INTERIOR, @@ -187,7 +186,7 @@ function NoteManager:init() auto_width=true, on_activate=function() self.subviews.comment:setFocus(true) end, }, - text_editor.TextEditor{ + widgets.TextArea{ view_id='comment', frame={t=6,b=3}, frame_style=gui.FRAME_INTERIOR, From 9eea352728006d60482bad15b8caeb6d841df75b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 12 Jan 2025 19:00:57 +0100 Subject: [PATCH 309/811] Migrate gui/notes to use widgets TextArea --- gui/notes.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 309130f97f..358af1de6c 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -8,7 +8,6 @@ local script = require 'gui.script' local overlay = require 'plugins.overlay' local utils = require 'utils' -local text_editor = reqscript('internal/journal/text_editor') local note_manager = reqscript('internal/notes/note_manager') local map_points = df.global.plotinfo.waypoints.points @@ -47,7 +46,7 @@ function NotesWindow:init() frame_inset={l=1,t=1,b=1,r=1}, autoarrange_subviews=true, subviews={ - text_editor.TextEditor{ + widgets.TextArea{ view_id='search', frame={l=0,h=3}, frame_style=gui.FRAME_INTERIOR, From 5a6f12628996fd55d04109208bfa259ac44c187d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 13 Jan 2025 21:17:30 -0800 Subject: [PATCH 310/811] refine word selector logic --- gui/rename.lua | 119 ++++++++++++++++++++++++++++--------------------- 1 file changed, 67 insertions(+), 52 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 907a5270a5..091573cf57 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -20,42 +20,62 @@ local translations = df.language_translation.get_vector() -- target selection -- -local wt = language.word_table +local entity_names = { + [df.language_name_type.Figure]=df.entity_name_type.OTHER, + [df.language_name_type.FigureFirstOnly]=df.entity_name_type.OTHER, + [df.language_name_type.FigureNoFirst]=df.entity_name_type.OTHER, + [df.language_name_type.Civilization]=df.entity_name_type.CIV, + [df.language_name_type.EntitySite]=df.entity_name_type.SITE, + [df.language_name_type.Site]=df.entity_name_type.OTHER, + [df.language_name_type.Squad]=df.entity_name_type.OTHER, + [df.language_name_type.Temple]=df.entity_name_type.TEMPLE, + [df.language_name_type.Library]=df.entity_name_type.LIBRARY, + [df.language_name_type.Hospital]=df.entity_name_type.HOSPITAL, +} -local function get_word_selectors(name_type, civ) - -- default to something generic with a lot of word choices - local major, minor = wt[0][df.language_name_category.River], wt[1][df.language_name_category.River] +local category_names = { + [df.language_name_type.World]=df.language_name_category.Region, + [df.language_name_type.Region]=df.language_name_category.Region, + [df.language_name_type.LegendaryFigure]=df.language_name_category.Unit, + [df.language_name_type.FigureNoFirst]=df.language_name_category.Unit, + [df.language_name_type.FigureFirstOnly]=df.language_name_category.Unit, + [df.language_name_type.Figure]=df.language_name_category.Unit, + [df.language_name_type.Religion]=df.language_name_category.CommonReligion, + [df.language_name_type.Temple]=df.language_name_category.Temple, + [df.language_name_type.FoodStore]=df.language_name_category.FoodStore, + [df.language_name_type.Library]=df.language_name_category.Library, + [df.language_name_type.Guildhall]=df.language_name_category.Guildhall, + [df.language_name_type.Hospital]=df.language_name_category.Hospital, +} - -- TODO: locations +local wt = language.word_table - -- refine word selector choice based on context - if name_type == df.language_name_type.Figure then - if civ then - major, minor = civ.entity_raw.symbols.symbols_major.OTHER, civ.entity_raw.symbols.symbols_minor.OTHER - else - major, minor = wt[0][df.language_name_category.Unit], wt[1][df.language_name_category.Unit] - end - elseif name_type == df.language_name_type.World then - major, minor = wt[0][df.language_name_category.Region], wt[1][df.language_name_category.Region] - elseif name_type == df.language_name_type.Artifact then +local function get_word_selectors(name_type, civ) + -- special cases + if name_type == df.language_name_type.Artifact then -- The game normally only uses ArtifactEvil if it was created by a fell/macabre mood, but we don't know -- at this point, so we'll randomize the choice if math.random(5) == 1 then - major, minor = wt[0][df.language_name_category.ArtifactEvil], wt[1][df.language_name_category.ArtifactEvil] + return wt[0][df.language_name_category.ArtifactEvil], wt[1][df.language_name_category.ArtifactEvil] else - major, minor = wt[0][df.language_name_category.Artifact], wt[1][df.language_name_category.Artifact] + return wt[0][df.language_name_category.Artifact], wt[1][df.language_name_category.Artifact] end - elseif name_type == df.language_name_type.Civilization and civ then - major, minor = civ.entity_raw.symbols.symbols_major.CIV, civ.entity_raw.symbols.symbols_minor.CIV - elseif name_type == df.language_name_type.EntitySite and civ then - major, minor = civ.entity_raw.symbols.symbols_major.SITE, civ.entity_raw.symbols.symbols_minor.SITE - elseif name_type == df.language_name_type.Site and civ then - major, minor = civ.entity_raw.symbols.symbols_major.OTHER, civ.entity_raw.symbols.symbols_minor.OTHER - elseif name_type == df.language_name_type.Squad and civ then - major, minor = civ.entity_raw.symbols.symbols_major.OTHER, civ.entity_raw.symbols.symbols_minor.OTHER end - return major, minor + -- entity-based names + local etype = entity_names[name_type] + if civ and etype then + return civ.entity_raw.symbols.symbols_major[etype], civ.entity_raw.symbols.symbols_minor[etype] + end + + -- category-based names + local ctype = category_names[name_type] + if ctype then + return wt[0][ctype], wt[1][ctype] + end + + -- default to something generic with a lot of word choices + return wt[0][df.language_name_category.River], wt[1][df.language_name_category.River] end local function get_artifact_target(item) @@ -64,8 +84,7 @@ local function get_artifact_target(item) if not gref then return end local rec = df.artifact_record.find(gref.artifact_id) if not rec then return end - local major_selector, minor_selector = get_word_selectors(df.language_name_type.Artifact) - return {name=rec.name, major_selector=major_selector, minor_selector=minor_selector} + return {name=rec.name} end local function get_hf_target(hf) @@ -79,9 +98,7 @@ local function get_hf_target(hf) table.insert(sync_names, unit_name) end end - local civ = df.historical_entity.find(hf.civ_id) - local major_selector, minor_selector = get_word_selectors(df.language_name_type.Figure, civ) - return {name=name, sync_names=sync_names, major_selector=major_selector, minor_selector=minor_selector} + return {name=name, sync_names=sync_names, civ_id=hf.civ_id} end local function get_unit_target(unit) @@ -91,31 +108,29 @@ local function get_unit_target(unit) return get_hf_target(hf) end -- unit with no hf - local civ = df.historical_entity.find(unit.civ_id) - local major_selector, minor_selector = get_word_selectors(df.language_name_type.Figure, civ) - return {name=dfhack.units.getVisibleName(unit), major_selector=major_selector, minor_selector=minor_selector} + return {name=dfhack.units.getVisibleName(unit), civ_id=unit.civ_id} end -local function get_civ_from_entity(entity) +local function get_civ_id_from_entity(entity) if not entity then return end - if entity.type == df.historical_entity_type.Civilization then return entity end + if entity.type == df.historical_entity_type.Civilization then return entity.id end for _,ee_link in ipairs(entity.entity_links) do if ee_link.type ~= df.entity_entity_link_type.PARENT then goto continue end local linked_he = df.historical_entity.find(ee_link.target) if linked_he and linked_he.type == df.historical_entity_type.Civilization then - return linked_he + return ee_link.target end ::continue:: end end -local function get_civ_from_site(site) +local function get_civ_id_from_site(site) if not site then return end for _,he_link in ipairs(site.entity_links) do if he_link.type ~= df.entity_site_link_type.All then goto continue end local linked_he = df.historical_entity.find(he_link.entity_id) if linked_he and linked_he.type == df.historical_entity_type.Civilization then - return linked_he + return he_link.entity_id end ::continue:: end @@ -123,27 +138,23 @@ end local function get_entity_target(entity) if not entity then return end - local major_selector, minor_selector = get_word_selectors(entity.name.type, get_civ_from_entity(entity)) - return {name=entity.name, major_selector=major_selector, minor_selector=minor_selector} + return {name=entity.name, civ_id=get_civ_id_from_entity(entity)} end local function get_site_target(site) if not site then return end - local major_selector, minor_selector = get_word_selectors(site.name.type, get_civ_from_site(site)) - return {name=site.name, major_selector=major_selector, minor_selector=minor_selector} + return {name=site.name, civ_id=get_civ_id_from_site(site)} end local function get_location_target(site, loc_id) if not site or loc_id < 0 then return end local loc = utils.binsearch(site.buildings, loc_id, 'id') if not loc then return end - local major_selector, minor_selector = get_word_selectors(loc.name.type, get_civ_from_site(site)) - return {name=loc.name, major_selector=major_selector, minor_selector=minor_selector} + return {name=loc.name, civ_id=get_civ_id_from_site(site)} end local function get_squad_target(fort, squad) - local major_selector, minor_selector = get_word_selectors(squad.name.type, get_civ_from_entity(fort)) - return {name=squad.name, major_selector=major_selector, minor_selector=minor_selector} + return {name=squad.name, civ_id=get_civ_id_from_entity(fort)} end local function get_world_target() @@ -155,8 +166,7 @@ local function get_world_target() dfhack.translation.translateName(name, true)) end } - local major_selector, minor_selector = get_word_selectors(df.language_name_type.World) - return {name=name, sync_names=sync_names, major_selector=major_selector, minor_selector=minor_selector} + return {name=name, sync_names=sync_names} end local function select_artifact(cb) @@ -251,10 +261,10 @@ local function select_new_target(cb) if #site.buildings > 0 then table.insert(choices, {text='A location', data={fn=curry(select_location, site)}}) end - table.insert(choices, {text='This fortress/site', data={fn=curry(select_site, site)}}) if fort and #fort.squads > 0 then table.insert(choices, {text='A squad', data={fn=curry(select_squad, fort)}}) end + table.insert(choices, {text='This fortress/site', data={fn=curry(select_site, site)}}) end if fort then table.insert(choices, {text='The government of this fortress', data={fn=curry(select_entity, fort)}}) @@ -712,8 +722,13 @@ function Rename:randomize_component_word(comp) end function Rename:generate_random_name() - dfhack.translation.generateName(self.target.name, self.target.name.language, self.target.name.type, - self.target.major_selector, self.target.minor_selector) + local civ + if self.target.civ_id then + civ = df.historical_entity.find(self.target.civ_id) + end + local major_selector, minor_selector = get_word_selectors(self.target.name.type, civ) + dfhack.translation.generateName(self.target.name, self.target.name.language, + self.target.name.type, major_selector, minor_selector) for _, sync_name in ipairs(self.target.sync_names) do if type(sync_name) == 'function' then sync_name() From 582c89d0b45b68ed1c76a71963a787c8ecee35b1 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 15 Jan 2025 10:13:26 -0600 Subject: [PATCH 311/811] add the lua side of the autosave code --- internal/notify/notifications.lua | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 0d312769d2..12b971aaf6 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -524,6 +524,22 @@ NOTIFICATIONS_BY_IDX = { adv_fn=curry(get_bar, get_blood, get_max_blood, "Blood", COLOR_RED), on_click=nil, }, + { + name='autosave', + desc='Shows a reminder to save now and then.', + default=true, + dwarf_fn=function () + local dur = dur or 8 + return "Save Reminder! Last Save: ".. dur ..' mins ago' + end, + on_click=function () + local dur = dur or 8 + local message = 'It has been ' .. dur .. ' mins since your last save. \n\nWould you like to save now? ' .. + '(Note: You can still close this reminder and save manually)' + dlg.showYesNoPrompt('Save now?', message, nil, function() + dfhack.run_script('quicksave') end) + end, + }, } NOTIFICATIONS_BY_NAME = {} From 0c85e31655134dcd9188e841f9d1e8a31065864d Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 15 Jan 2025 11:42:48 -0600 Subject: [PATCH 312/811] Add time calcs. --- internal/notify/notifications.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 12b971aaf6..9e31bddbbd 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -14,6 +14,8 @@ local buildings = df.global.world.buildings local caravans = df.global.plotinfo.caravans local units = df.global.world.units +SaveDur = 0 + function for_iter(vec, match_fn, action_fn, reverse) local offset = type(vec) == 'table' and 1 or 0 local idx1 = reverse and #vec-1+offset or offset @@ -529,12 +531,12 @@ NOTIFICATIONS_BY_IDX = { desc='Shows a reminder to save now and then.', default=true, dwarf_fn=function () - local dur = dur or 8 - return "Save Reminder! Last Save: ".. dur ..' mins ago' + local durMS = getTickCount() - getSaveTick() + SaveDur = durMS / (60 * 1000) -- 60 seconds, 1000 ms in a second + return "Save Reminder! Last Save: ".. SaveDur ..' mins ago' end, on_click=function () - local dur = dur or 8 - local message = 'It has been ' .. dur .. ' mins since your last save. \n\nWould you like to save now? ' .. + local message = 'It has been ' .. SaveDur .. ' mins since your last save. \n\nWould you like to save now? ' .. '(Note: You can still close this reminder and save manually)' dlg.showYesNoPrompt('Save now?', message, nil, function() dfhack.run_script('quicksave') end) From 4d6f9e5dbf3d674e593b2aba7e7d49b728472d28 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 15 Jan 2025 12:53:15 -0600 Subject: [PATCH 313/811] Update notifications.lua --- internal/notify/notifications.lua | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 9e31bddbbd..a90de9c981 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -531,12 +531,10 @@ NOTIFICATIONS_BY_IDX = { desc='Shows a reminder to save now and then.', default=true, dwarf_fn=function () - local durMS = getTickCount() - getSaveTick() - SaveDur = durMS / (60 * 1000) -- 60 seconds, 1000 ms in a second - return "Save Reminder! Last Save: ".. SaveDur ..' mins ago' + return "Save Reminder! Last Save: ".. dfhack.persistence.getCurSaveDur() ..' mins ago' end, on_click=function () - local message = 'It has been ' .. SaveDur .. ' mins since your last save. \n\nWould you like to save now? ' .. + local message = 'It has been ' .. dfhack.persistence.getCurSaveDur() .. ' mins since your last save. \n\nWould you like to save now? ' .. '(Note: You can still close this reminder and save manually)' dlg.showYesNoPrompt('Save now?', message, nil, function() dfhack.run_script('quicksave') end) From 8d00171b0461d93895f743c1c873c72f6931470f Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:13:34 -0600 Subject: [PATCH 314/811] Added all the logic based on the added api --- internal/notify/notifications.lua | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index a90de9c981..93ca5202a2 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -531,10 +531,15 @@ NOTIFICATIONS_BY_IDX = { desc='Shows a reminder to save now and then.', default=true, dwarf_fn=function () - return "Save Reminder! Last Save: ".. dfhack.persistence.getCurSaveDur() ..' mins ago' + local minsSinceSave = dfhack.persistent.getUnsavedSeconds()//60 + if minsSinceSave < 15 then + return nil + end + return "Last save: ".. (dfhack.formatInt(minsSinceSave)) ..' mins ago' end, on_click=function () - local message = 'It has been ' .. dfhack.persistence.getCurSaveDur() .. ' mins since your last save. \n\nWould you like to save now? ' .. + local minsSinceSave = dfhack.persistent.getUnsavedSeconds()//60 + local message = 'It has been ' .. (dfhack.formatInt(minsSinceSave)) .. ' mins since your last save. \n\nWould you like to save now? ' .. '(Note: You can still close this reminder and save manually)' dlg.showYesNoPrompt('Save now?', message, nil, function() dfhack.run_script('quicksave') end) From 64ee438e2c9610860dc39cd7531a8dcae280e79e Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 15 Jan 2025 16:17:49 -0600 Subject: [PATCH 315/811] remove the whitespace --- internal/notify/notifications.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 93ca5202a2..b6138dc415 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -535,7 +535,7 @@ NOTIFICATIONS_BY_IDX = { if minsSinceSave < 15 then return nil end - return "Last save: ".. (dfhack.formatInt(minsSinceSave)) ..' mins ago' + return "Last save: ".. (dfhack.formatInt(minsSinceSave)) ..' mins ago' end, on_click=function () local minsSinceSave = dfhack.persistent.getUnsavedSeconds()//60 From c5d180872e8c571e1c0cc0058363ae0009d0fbca Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:31:32 -0600 Subject: [PATCH 316/811] Apply suggestions from code review Co-authored-by: Myk --- internal/notify/notifications.lua | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index b6138dc415..08e6c1eb0b 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -528,21 +528,19 @@ NOTIFICATIONS_BY_IDX = { }, { name='autosave', - desc='Shows a reminder to save now and then.', + desc='Shows a reminder if it has been more than 15 minutes since your last save.', default=true, dwarf_fn=function () local minsSinceSave = dfhack.persistent.getUnsavedSeconds()//60 - if minsSinceSave < 15 then - return nil + if minsSinceSave >= 15 then + return "Last save: ".. (dfhack.formatInt(minsSinceSave)) ..' mins ago' end - return "Last save: ".. (dfhack.formatInt(minsSinceSave)) ..' mins ago' end, - on_click=function () + on_click=function() local minsSinceSave = dfhack.persistent.getUnsavedSeconds()//60 - local message = 'It has been ' .. (dfhack.formatInt(minsSinceSave)) .. ' mins since your last save. \n\nWould you like to save now? ' .. - '(Note: You can still close this reminder and save manually)' - dlg.showYesNoPrompt('Save now?', message, nil, function() - dfhack.run_script('quicksave') end) + local message = 'It has been ' .. dfhack.formatInt(minsSinceSave) .. ' minutes since your last save. \n\nWould you like to save now? ' .. + '(Note: You can also close this reminder and save manually)' + dlg.showYesNoPrompt('Save now?', message, nil, function() dfhack.run_script('quicksave') end) end, }, } From 67b3f227d83ebe2e083cceaf9741e66b011e174d Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 15 Jan 2025 18:35:07 -0600 Subject: [PATCH 317/811] Removed old global and renamed the notification --- internal/notify/notifications.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 08e6c1eb0b..a527b43549 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -14,8 +14,6 @@ local buildings = df.global.world.buildings local caravans = df.global.plotinfo.caravans local units = df.global.world.units -SaveDur = 0 - function for_iter(vec, match_fn, action_fn, reverse) local offset = type(vec) == 'table' and 1 or 0 local idx1 = reverse and #vec-1+offset or offset @@ -527,7 +525,7 @@ NOTIFICATIONS_BY_IDX = { on_click=nil, }, { - name='autosave', + name='save-reminder', desc='Shows a reminder if it has been more than 15 minutes since your last save.', default=true, dwarf_fn=function () From c577e9b25b55c8e212ac7c63697b8b2a2e0104d9 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Wed, 15 Jan 2025 21:56:14 -0600 Subject: [PATCH 318/811] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 97c2325dad..44bb9711e4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: - `gui/settings-manager`: standing orders save/load now includes the reserved barrels setting - `gui/rename`: add overlay to worldgen screen allowing you to rename the world before the new world is saved - `gui/rename`: add overlay to the "Prepare carefully" embark screen that transparently fixes a DF bug where you can't give units nicknames or custom professions +- `gui/notify`: new notification type: save reminder; appears 15 minutes after a save is created or loaded, click to be asked to quicksave ## Fixes - `fix/dry-buckets`: don't empty buckets for wells that are actively in use From 6b9daef9ec08ba2fca0d636a90ea9fb9734ac022 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 15 Jan 2025 20:07:53 -0800 Subject: [PATCH 319/811] changelog editing pass --- changelog.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 44bb9711e4..4bea5b4a66 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,13 +35,13 @@ Template for new versions: - `gui/settings-manager`: standing orders save/load now includes the reserved barrels setting - `gui/rename`: add overlay to worldgen screen allowing you to rename the world before the new world is saved - `gui/rename`: add overlay to the "Prepare carefully" embark screen that transparently fixes a DF bug where you can't give units nicknames or custom professions -- `gui/notify`: new notification type: save reminder; appears 15 minutes after a save is created or loaded, click to be asked to quicksave +- `gui/notify`: new notification type: save reminder; appears if you have gone more than 15 minutes without saving; click to autosave ## Fixes - `fix/dry-buckets`: don't empty buckets for wells that are actively in use - `gui/unit-info-viewer`: skill progress bars now show correct XP thresholds for skills past Legendary+5 - `caravan`: no longer incorrectly identify wood-based plant items and plant-based soaps as being ethically unsuitable for trading with the elves -- `gui/design`: don't require an extra right click when canceling building area designations +- `gui/design`: don't require an extra right click on the first cancel of building area designations - `gui/gm-unit`: refresh unit sprite when profession is changed ## Misc Improvements From 53e4ed2d8a5cd811638454410e0c2015d3f4e746 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 16 Jan 2025 07:33:04 +0100 Subject: [PATCH 320/811] Migrate notes manager to TextArea widget --- internal/notes/note_manager.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/notes/note_manager.lua b/internal/notes/note_manager.lua index 9df64d9694..6ba76555be 100644 --- a/internal/notes/note_manager.lua +++ b/internal/notes/note_manager.lua @@ -3,11 +3,11 @@ local gui = require('gui') local widgets = require('gui.widgets') local guidm = require('gui.dwarfmode') -local text_editor = reqscript('internal/journal/text_editor') local waypoints = df.global.plotinfo.waypoints local map_points = df.global.plotinfo.waypoints.points + NoteManager = defclass(NoteManager, gui.ZScreen) NoteManager.ATTRS{ focus_path='notes/note-manager', @@ -34,7 +34,7 @@ function NoteManager:init() auto_width=true, on_activate=function() self.subviews.name:setFocus(true) end, }, - text_editor.TextEditor{ + widgets.TextArea{ view_id='name', frame={t=1,h=3}, frame_style=gui.FRAME_INTERIOR, @@ -49,7 +49,7 @@ function NoteManager:init() auto_width=true, on_activate=function() self.subviews.comment:setFocus(true) end, }, - text_editor.TextEditor{ + widgets.TextArea{ view_id='comment', frame={t=6,b=3}, frame_style=gui.FRAME_INTERIOR, From 81e5179d29ef12f8ea44d8efee35bd34157b9014 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Thu, 16 Jan 2025 12:25:44 -0600 Subject: [PATCH 321/811] Move code to new files --- devel/helloSlider.lua | 143 +----------------------------------------- devel/slider.lua | 132 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 133 insertions(+), 142 deletions(-) create mode 100644 devel/slider.lua diff --git a/devel/helloSlider.lua b/devel/helloSlider.lua index f22b7c7a6b..e2e250df6b 100644 --- a/devel/helloSlider.lua +++ b/devel/helloSlider.lua @@ -1,144 +1,3 @@ -local Widget = require('gui.widgets.widget') - -local to_pen = dfhack.pen.parse - --------------------------------- --- Slider --------------------------------- - ----@class widgets.Slider.attrs: widgets.Widget.attrs ----@field num_stops integer ----@field get_idx_fn? function ----@field on_change? fun(index: integer) - ----@class widgets.Slider.attrs.partial: widgets.Slider.attrs - ----@class widgets.Slider.initTable: widgets.Slider.attrs ----@field num_stops integer - ----@class widgets.Slider: widgets.Widget, widgets.Slider.attrs ----@field super widgets.Widget ----@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) ----@overload fun(init_table: widgets.Slider.initTable): self -Slider = defclass(Slider, Widget) -Slider.ATTRS{ - num_stops=DEFAULT_NIL, - get_idx_fn=DEFAULT_NIL, - on_change=DEFAULT_NIL, -} - -function Slider:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 -end - -function Slider:init() - if self.num_stops < 2 then error('too few Slider stops') end - self.is_dragging_target = nil -- 'left', 'right', or 'both' - self.is_dragging_idx = nil -- offset from leftmost dragged tile -end - -local function Slider_get_width_per_idx(self) - return math.max(3, (self.frame_body.width-7) // (self.num_stops-1)) -end - -function Slider:onInput(keys) - if not keys._MOUSE_L then return false end - local x = self:getMousePos() - if not x then return false end - local left_idx = self.get_idx_fn() - local width_per_idx = Slider_get_width_per_idx(self) - local left_pos = width_per_idx*(left_idx-1) - local right_pos = width_per_idx*(left_idx-1) + 4 - if x < left_pos then - self.on_change(self.get_idx_fn() - 1) - else - self.is_dragging_target = 'both' - self.is_dragging_idx = x - right_pos - end - return true -end - -local function Slider_do_drag(self, width_per_idx) - local x = self.frame_body:localXY(dfhack.screen.getMousePos()) - local cur_pos = x - self.is_dragging_idx - cur_pos = math.max(0, cur_pos) - cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) - local offset = 1 - local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 - if self.is_dragging_target == 'both' then - if new_idx > self.num_stops then - return - end - end - if new_idx and new_idx ~= self.get_idx_fn() then - self.on_change(new_idx) - end -end - -local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} - -function Slider:onRenderBody(dc, rect) - local left_idx = self.get_idx_fn() - local width_per_idx = Slider_get_width_per_idx(self) - -- draw track - dc:seek(1,0) - dc:char(nil, SLIDER_LEFT_END) - dc:char(nil, SLIDER_TRACK) - for stop_idx=1,self.num_stops-1 do - local track_stop_pen = SLIDER_TRACK_STOP_SELECTED - local track_pen = SLIDER_TRACK_SELECTED - if left_idx ~= stop_idx then - track_stop_pen = SLIDER_TRACK_STOP - track_pen = SLIDER_TRACK - elseif left_idx == stop_idx then - track_pen = SLIDER_TRACK - end - dc:char(nil, track_stop_pen) - for i=2,width_per_idx do - dc:char(nil, track_pen) - end - end - if left_idx >= self.num_stops then - dc:char(nil, SLIDER_TRACK_STOP_SELECTED) - else - dc:char(nil, SLIDER_TRACK_STOP) - end - dc:char(nil, SLIDER_TRACK) - dc:char(nil, SLIDER_RIGHT_END) - -- draw tab - dc:seek(width_per_idx*(left_idx-1)+2) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - -- manage dragging - if self.is_dragging_target then - Slider_do_drag(self, width_per_idx) - end - if df.global.enabler.mouse_lbut_down == 0 then - self.is_dragging_target = nil - self.is_dragging_idx = nil - end -end - - - - - - - - - - - local gui = require('gui') local widgets = require('gui.widgets') @@ -177,7 +36,7 @@ function RangerWindow:init() self.subviews.level:setOption(val) end, }, - Slider{ + widgets.Slider{ frame={l=1, t=3}, num_stops=#LEVEL_OPTIONS, get_idx_fn=function() diff --git a/devel/slider.lua b/devel/slider.lua new file mode 100644 index 0000000000..92263d2924 --- /dev/null +++ b/devel/slider.lua @@ -0,0 +1,132 @@ +local Widget = require('gui.widgets.widget') + +local to_pen = dfhack.pen.parse + +-------------------------------- +-- Slider +-------------------------------- + +---@class widgets.Slider.attrs: widgets.Widget.attrs +---@field num_stops integer +---@field get_idx_fn? function +---@field on_change? fun(index: integer) + +---@class widgets.Slider.attrs.partial: widgets.Slider.attrs + +---@class widgets.Slider.initTable: widgets.Slider.attrs +---@field num_stops integer + +---@class widgets.Slider: widgets.Widget, widgets.Slider.attrs +---@field super widgets.Widget +---@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) +---@overload fun(init_table: widgets.Slider.initTable): self +Slider = defclass(Slider, Widget) +Slider.ATTRS{ + num_stops=DEFAULT_NIL, + get_idx_fn=DEFAULT_NIL, + on_change=DEFAULT_NIL, +} + +function Slider:preinit(init_table) + init_table.frame = init_table.frame or {} + init_table.frame.h = init_table.frame.h or 1 +end + +function Slider:init() + if self.num_stops < 2 then error('too few Slider stops') end + self.is_dragging_target = nil -- 'left', 'right', or 'both' + self.is_dragging_idx = nil -- offset from leftmost dragged tile +end + +local function Slider_get_width_per_idx(self) + return math.max(3, (self.frame_body.width-7) // (self.num_stops-1)) +end + +function Slider:onInput(keys) + if not keys._MOUSE_L then return false end + local x = self:getMousePos() + if not x then return false end + local left_idx = self.get_idx_fn() + local width_per_idx = Slider_get_width_per_idx(self) + local left_pos = width_per_idx*(left_idx-1) + local right_pos = width_per_idx*(left_idx-1) + 4 + if x < left_pos then + self.on_change(self.get_idx_fn() - 1) + else + self.is_dragging_target = 'both' + self.is_dragging_idx = x - right_pos + end + return true +end + +local function Slider_do_drag(self, width_per_idx) + local x = self.frame_body:localXY(dfhack.screen.getMousePos()) + local cur_pos = x - self.is_dragging_idx + cur_pos = math.max(0, cur_pos) + cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) + local offset = 1 + local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 + if self.is_dragging_target == 'both' then + if new_idx > self.num_stops then + return + end + end + if new_idx and new_idx ~= self.get_idx_fn() then + self.on_change(new_idx) + end +end + +local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} +local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} +local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} +local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} + +function Slider:onRenderBody(dc, rect) + local left_idx = self.get_idx_fn() + local width_per_idx = Slider_get_width_per_idx(self) + -- draw track + dc:seek(1,0) + dc:char(nil, SLIDER_LEFT_END) + dc:char(nil, SLIDER_TRACK) + for stop_idx=1,self.num_stops-1 do + local track_stop_pen = SLIDER_TRACK_STOP_SELECTED + local track_pen = SLIDER_TRACK_SELECTED + if left_idx ~= stop_idx then + track_stop_pen = SLIDER_TRACK_STOP + track_pen = SLIDER_TRACK + elseif left_idx == stop_idx then + track_pen = SLIDER_TRACK + end + dc:char(nil, track_stop_pen) + for i=2,width_per_idx do + dc:char(nil, track_pen) + end + end + if left_idx >= self.num_stops then + dc:char(nil, SLIDER_TRACK_STOP_SELECTED) + else + dc:char(nil, SLIDER_TRACK_STOP) + end + dc:char(nil, SLIDER_TRACK) + dc:char(nil, SLIDER_RIGHT_END) + -- draw tab + dc:seek(width_per_idx*(left_idx-1)+2) + dc:char(nil, SLIDER_TAB_LEFT) + dc:char(nil, SLIDER_TAB_CENTER) + dc:char(nil, SLIDER_TAB_RIGHT) + -- manage dragging + if self.is_dragging_target then + Slider_do_drag(self, width_per_idx) + end + if df.global.enabler.mouse_lbut_down == 0 then + self.is_dragging_target = nil + self.is_dragging_idx = nil + end +end + +return Slider From a2895a8061558c7c599a3addd186e338f4915d0d Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 16 Jan 2025 20:09:17 +0100 Subject: [PATCH 322/811] new tool: autocheese --- autocheese.lua | 155 ++++++++++++++++++++++++++++ changelog.txt | 1 + docs/autocheese.rst | 38 +++++++ internal/control-panel/registry.lua | 2 + 4 files changed, 196 insertions(+) create mode 100644 autocheese.lua create mode 100644 docs/autocheese.rst diff --git a/autocheese.lua b/autocheese.lua new file mode 100644 index 0000000000..fa01fc53d6 --- /dev/null +++ b/autocheese.lua @@ -0,0 +1,155 @@ +--@module = true + +local ic = reqscript('idle-crafting') + +---make cheese using a specific barrel and workshop +---@param barrel df.item +---@param workshop df.building_workshopst +---@return df.job +function makeCheese(barrel, workshop) + ---@type df.job + local job = ic.make_job() + job.job_type = df.job_type.MakeCheese + + local jitem = df.job_item:new() + jitem.quantity = 0 + jitem.vector_id = df.job_item_vector_id.ANY_COOKABLE + jitem.flags1.unrotten = true + jitem.flags1.milk = true + job.job_items.elements:insert('#', jitem) + + if not dfhack.job.attachJobItem(job, barrel, df.job_item_ref.T_role.Reagent, 0, -1) then + dfhack.error('could not attach item') + end + + ic.assignToWorkshop(job, workshop) + return job +end + + + +---unit is ready to take jobs +---@param unit df.unit +---@return boolean +function unitIsAvailable(unit) + if unit.job.current_job then + return false + elseif #unit.individual_drills > 0 then + return false + elseif unit.flags1.caged or unit.flags1.chained then + return false + elseif unit.military.squad_id ~= -1 then + local squad = df.squad.find(unit.military.squad_id) + -- this lookup should never fail + ---@diagnostic disable-next-line: need-check-nil + return #squad.orders == 0 and squad.activity == -1 + end + return true +end + +---find unit with a particular labor enabled +---@param unit_labor df.unit_labor +---@param job_skill df.job_skill +---@param workshop df.building +---@return df.unit|nil +---@return integer|nil + function findAvailableLaborer(unit_labor, job_skill, workshop) + local max_unit = nil + local max_skill = -1 + for _, unit in ipairs(dfhack.units.getCitizens(true, false)) do + if + unit.status.labors[unit_labor] and + unitIsAvailable(unit) and + ic.canAccessWorkshop(unit, workshop) + then + local unit_skill = dfhack.units.getNominalSkill(unit, job_skill, true) + if unit_skill > max_skill then + max_unit = unit + max_skill = unit_skill + end + end + end + return max_unit, max_skill +end + +local function findMilkBarrel(min_liquids) + for _, container in ipairs(df.global.world.items.other.FOOD_STORAGE) do + if + not (container.flags.in_job or container.flags.forbid) and + container.flags.container and #container.general_refs >= min_liquids + then + local content_reference = dfhack.items.getGeneralRef(container, df.general_ref_type.CONTAINS_ITEM) + local contained_item = df.item.find(content_reference and content_reference.item_id or -1) + if contained_item then + local mat_info = dfhack.matinfo.decode(contained_item) + if mat_info:matches { milk = true } then + return container + end + end + end + end +end + +function findWorkshop() + for _,workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do + if + not workshop.profile.blocked_labors[df.unit_labor.MAKE_CHEESE] and + #workshop.jobs == 0 and #workshop.profile.permitted_workers == 0 + then + return workshop + end + end +end + +if dfhack_flags.module then + return +end + +-- actual script action + +local argparse = require('argparse') + +local min_number = 50 + +local _ = argparse.processArgsGetopt({...}, +{ + { 'm', 'min-milk', hasArg = true, + handler = function(min) + min_number = argparse.nonnegativeInt(min, 'min-milk') + end } +}) + + +local reagent = findMilkBarrel(min_number) + +if not reagent then + -- print('autocheese: no sufficiently full barrel found') + return +end + +local workshop = findWorkshop() + +if not workshop then + print('autocheese: no workshop available') + return +end + +local worker, skill = findAvailableLaborer(df.unit_labor.MAKE_CHEESE, df.job_skill.CHEESEMAKING, workshop) +if not worker then + print('autocheese: no cheesemaker available') + return +end +local job = makeCheese(reagent, workshop) + +print(('autocheese: dispatching cheesemaking job for %s (%d milk) to %s'):format( + dfhack.items.getDescription(reagent, 0), + #reagent.general_refs, + dfhack.df2console(dfhack.units.getReadableName(worker)) +)) + + +-- assign a worker and send it to fetch the barrel +dfhack.job.addWorker(job, worker) +dfhack.units.setPathGoal(worker, reagent.pos, df.unit_path_goal.GrabJobResources) +job.items[0].flags.is_fetching = true +job.flags.fetching = true diff --git a/changelog.txt b/changelog.txt index 4bea5b4a66..95a560069a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools - `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map - `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (e.g. units, governments, fortresses, or the world) +- `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk ## New Features - `gui/settings-manager`: new overlay on the Labor -> Standing Orders tab for configuring the number of barrels to reserve for job use (so you can brew alcohol and not have all your barrels claimed by stockpiles for container storage) diff --git a/docs/autocheese.rst b/docs/autocheese.rst new file mode 100644 index 0000000000..3ef926b5fc --- /dev/null +++ b/docs/autocheese.rst @@ -0,0 +1,38 @@ +autocheese +========== + +.. dfhack-tool:: + :summary: Automatically make cheese using barrels that have accumulated sufficient milk. + :tags: fort auto + +Cheese making is difficult to automate using work orders, because a single job +can consume anything from a bucket was a single unit of milk to barrel +containing up to 100 units of milk. + +The script will scan your fort for barrels with a certain minimum amount of milk +(default: 50), create a cheese making job specifically for that barrel, and +assign this job to one of your idle dwarves (giving preference to skilled cheese +makers). + +When enabled using `gui/control-panel`, the script will run automatically, with +default options, twice a month. + +Usage +----- + +:: + + autocheese [] + +Examples +-------- + +``autocheese -m 100`` + Only create a job if there is a barrel that is filled to the maximum. + +Options +------- + +``-m``, ``--min-milk`` + Set the minimum number of milk items in a barrel for the barrel to be + considered for cheese making. \ No newline at end of file diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 04b28b3ff5..6ffd0e3a7b 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -18,6 +18,8 @@ COMMANDS_BY_IDX = { {command='autobutcher target 10 10 14 2 BIRD_PEAFOWL_BLUE', group='automation', mode='run', desc='Enable if you usually want to raise peafowl.'}, {command='autochop', group='automation', mode='enable'}, + {command='autocheese', group='automation', mode='repeat', + params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'autocheese', ']'}}, {command='autoclothing', group='automation', mode='enable'}, {command='autofarm', group='automation', mode='enable'}, {command='autofarm threshold 150 grass_tail_pig', group='automation', mode='run', From 70240fe3fbdf6ead3f3eeea78ef769ec675ba7af Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Thu, 16 Jan 2025 19:22:29 +0000 Subject: [PATCH 323/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/autocheese.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/autocheese.rst b/docs/autocheese.rst index 3ef926b5fc..b96ffd6510 100644 --- a/docs/autocheese.rst +++ b/docs/autocheese.rst @@ -35,4 +35,4 @@ Options ``-m``, ``--min-milk`` Set the minimum number of milk items in a barrel for the barrel to be - considered for cheese making. \ No newline at end of file + considered for cheese making. From 9fb2e828157d2546bb526d3d326daad1d739a0f9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Jan 2025 19:08:30 -0800 Subject: [PATCH 324/811] changelog editing pass --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 4bea5b4a66..6c29134b94 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,7 +28,7 @@ Template for new versions: ## New Tools - `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map -- `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (e.g. units, governments, fortresses, or the world) +- `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (units, governments, fortresses, the world, etc.) ## New Features - `gui/settings-manager`: new overlay on the Labor -> Standing Orders tab for configuring the number of barrels to reserve for job use (so you can brew alcohol and not have all your barrels claimed by stockpiles for container storage) From 99d714e135758bda076dca871c6b3d85229b6e33 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 16 Jan 2025 19:57:28 -0800 Subject: [PATCH 325/811] reorder items in selection dialog --- gui/rename.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/gui/rename.lua b/gui/rename.lua index 091573cf57..40ce109018 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -253,17 +253,20 @@ local function select_new_target(cb) if #df.global.world.items.other.ANY_ARTIFACT > 0 then table.insert(choices, {text='An artifact', data={fn=select_artifact}}) end + if #df.global.world.units.all > 0 then + table.insert(choices, {text='A unit', data={fn=select_unit}}) + end local site = dfhack.world.getCurrentSite() local is_fort_mode = dfhack.world.isFortressMode() local fort = is_fort_mode and df.historical_entity.find(df.global.plotinfo.group_id) local civ = is_fort_mode and df.historical_entity.find(df.global.plotinfo.civ_id) if site then - if #site.buildings > 0 then - table.insert(choices, {text='A location', data={fn=curry(select_location, site)}}) - end if fort and #fort.squads > 0 then table.insert(choices, {text='A squad', data={fn=curry(select_squad, fort)}}) end + if #site.buildings > 0 then + table.insert(choices, {text='A location', data={fn=curry(select_location, site)}}) + end table.insert(choices, {text='This fortress/site', data={fn=curry(select_site, site)}}) end if fort then @@ -272,10 +275,7 @@ local function select_new_target(cb) if civ then table.insert(choices, {text='The civilization of this fortress', data={fn=curry(select_entity, civ)}}) end - if #df.global.world.units.all > 0 then - table.insert(choices, {text='A unit', data={fn=select_unit}}) - end - table.insert(choices, {text='This world', data={fn=select_world}}) + table.insert(choices, {text='The world', data={fn=select_world}}) dlg.showListPrompt('Rename', 'What would you like to rename?', COLOR_WHITE, choices, function(_, choice) choice.data.fn(cb) end) end From fbcb217a533a7fb08532f6f000a5acb719feee5f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 17 Jan 2025 08:22:04 -0800 Subject: [PATCH 326/811] bump changelog to 50.15-r2 --- changelog.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 6c29134b94..e8ad3bfc33 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,18 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 50.15-r2 + ## New Tools - `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map - `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (units, governments, fortresses, the world, etc.) @@ -50,8 +62,6 @@ Template for new versions: - `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) - `gui/sitemap`: show primary group affiliation for visitors and invaders (e.g. civilization name or performance troupe) -## Removed - # 50.15-r1 # 50.14-r2 From 5b7e692ba5a6ef1f9ba1a569bb11b42cf75b6f1b Mon Sep 17 00:00:00 2001 From: Bjorn Macintosh Date: Sun, 19 Jan 2025 01:17:42 +1000 Subject: [PATCH 327/811] Feat: Add "show" to pref-adjust This feature returns the list of Likes for a selected dwarf. This script may be useful if: * a user wants to view a quick list of likes for a dwarf * a user wants to view the list of likes after updating a dwarfs likes * Other scripts may find this helpful, as this functionality is not otherwise exposed via dfhack --- pref-adjust.lua | 50 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 49 insertions(+), 1 deletion(-) diff --git a/pref-adjust.lua b/pref-adjust.lua index 067376a008..b94f709c7a 100644 --- a/pref-adjust.lua +++ b/pref-adjust.lua @@ -6,7 +6,6 @@ pss_counter = pss_counter or 31415926 -- --------------------------------------------------------------------------- function insert_preference(unit, preftype, val1) - if preftype == df.unit_preference.T_type.LikeMaterial then utils.insert_or_update(unit.status.current_soul.preferences, { new = true, @@ -269,6 +268,51 @@ function build_all_lists(printflag) end end -- end func build_all_lists -- --------------------------------------------------------------------------- +function get_preferences(unit) + if unit == nil then + print("No unit selected!") + return + end + + local preferences = unit.status.current_soul.preferences + if #preferences == 0 then + print("Unit " .. unit_name_to_console(unit) .. " has no preferences.") + return + end + + print("Preferences for " .. unit_name_to_console(unit) .. ":") + for _, pref in ipairs(preferences) do + local pref_type = df.unit_preference.T_type[pref.type] + local description = "" + + if pref_type == "LikeMaterial" then + description = "Likes material: " .. dfhack.matinfo.getToken(pref.mattype, pref.matindex) + elseif pref_type == "LikeFood" then + description = "Likes food: " .. dfhack.matinfo.getToken(pref.mattype, pref.matindex) + elseif pref_type == "LikeItem" then + description = "Likes item type: " .. tostring(pref.item_type) + elseif pref_type == "LikePlant" then + description = "Likes plant: " .. dfhack.matinfo.getToken(pref.mattype, pref.matindex) + elseif pref_type == "HateCreature" then + description = "Hates creature: " .. df.global.world.raws.creatures.all[pref.poetic_form_id].creature_id + elseif pref_type == "LikeColor" then + description = "Likes color: " .. df.global.world.raws.descriptors.colors[pref.poetic_form_id].id + elseif pref_type == "LikeShape" then + description = "Likes shape: " .. df.global.world.raws.descriptors.shapes[pref.poetic_form_id].id + elseif pref_type == "LikePoeticForm" then + description = "Likes poetic form: " .. dfhack.translation.translateName(df.global.world.poetic_forms.all[pref.poetic_form_id].name, true) + elseif pref_type == "LikeMusicalForm" then + description = "Likes musical form: " .. dfhack.translation.translateName(df.global.world.musical_forms.all[pref.poetic_form_id].name, true) + elseif pref_type == "LikeDanceForm" then + description = "Likes dance form: " .. dfhack.translation.translateName(df.global.world.dance_forms.all[pref.poetic_form_id].name, true) + else + description = "Unknown preference type: " .. tostring(pref.type) + end + + print(description) + end +end -- end function: get_preferences +-- --------------------------------------------------------------------------- function unit_name_to_console(unit) return dfhack.df2console(dfhack.units.getReadableName(unit)) end @@ -317,10 +361,14 @@ elseif opt == "all" then handle_all("IDEAL") elseif opt == "goth_all" then handle_all("GOTH") +if opt == "show" then + local unit = dfhack.gui.getSelectedUnit() + get_preferences(unit) else print ("Sets preferences of one dwarf, or of all dwarves, using profiles.") print ("Valid options:") print ("list -- show available preference type lists") + print ("show -- show current preferences") print ("clear -- clear preferences of selected unit") print ("clear_all -- clear preferences of all units") print ("goth -- alter current dwarf preferences to Goth") From 90327062f5e451f5a0b804c984c242a84148f32d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 18 Jan 2025 09:16:22 -0800 Subject: [PATCH 328/811] fix typo in API call --- changelog.txt | 1 + deathcause.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index e8ad3bfc33..45dacce451 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `deathcause`: fix error when retrieving the name of a historical figure ## Misc Improvements diff --git a/deathcause.lua b/deathcause.lua index 16e05d5f5a..953f36bd22 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -75,7 +75,7 @@ function displayDeathEventHistFigUnit(histfig_unit, event) if slayer_histfig then str = str .. (", killed by the %s %s"):format( getRaceNameSingular(slayer_histfig.race), - dfhack.translation.translateName(dfhack.units.getVisiblename(slayer_histfig)) + dfhack.translation.translateName(dfhack.units.getVisibleName(slayer_histfig)) ) end From 66d8d3915ce83d2a69f71f5d5ab91b8cd8627588 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 18 Jan 2025 23:44:18 +0100 Subject: [PATCH 329/811] make possible to show specific stress/happiness levels; add config UI --- gui/tooltips.lua | 165 +++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 138 insertions(+), 27 deletions(-) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index d9015afff0..cf1b861f82 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -11,19 +11,79 @@ local ResizingPanel = require('gui.widgets.containers.resizing_panel') -------------------------------------------------------------------------------- -config = config or { - follow_units = true, - follow_mouse = true, -} -local function change_follow_units(new, old) - config.follow_units = new +-- pens are the same as gui/control-panel.lua +local textures = require('gui.textures') +local function get_icon_pens() + local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} + local enabled_pen_center = dfhack.pen.parse{fg=COLOR_LIGHTGREEN, + tile=curry(textures.tp_control_panel, 2) or nil, ch=251} -- check + local enabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 3) or nil, ch=string.byte(']')} + local disabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 4) or nil, ch=string.byte('[')} + local disabled_pen_center = dfhack.pen.parse{fg=COLOR_RED, + tile=curry(textures.tp_control_panel, 5) or nil, ch=string.byte('x')} + local disabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 6) or nil, ch=string.byte(']')} + local button_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} + local button_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} + local help_pen_center = dfhack.pen.parse{ + tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} + local configure_pen_center = dfhack.pen.parse{ + tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol + return enabled_pen_left, enabled_pen_center, enabled_pen_right, + disabled_pen_left, disabled_pen_center, disabled_pen_right, + button_pen_left, button_pen_right, + help_pen_center, configure_pen_center end -local function change_follow_mouse(new, old) - config.follow_mouse = new +local ENABLED_PEN_LEFT, ENABLED_PEN_CENTER, ENABLED_PEN_RIGHT, + DISABLED_PEN_LEFT, DISABLED_PEN_CENTER, DISABLED_PEN_RIGHT, + BUTTON_PEN_LEFT, BUTTON_PEN_RIGHT, + HELP_PEN_CENTER, CONFIGURE_PEN_CENTER = get_icon_pens() + +if RELOAD then ToggleLabel = nil end +ToggleLabel = defclass(ToggleLabel, widgets.CycleHotkeyLabel) +ToggleLabel.ATTRS{ + options={{value=true}, + {value=false}}, +} +function ToggleLabel:init() + ToggleLabel.super.init(self) + + local text = self.text + -- the very last token is the On/Off text -- we'll repurpose it as an indicator + text[#text] = { tile = function() return self:getOptionValue() and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT end } + text[#text + 1] = { tile = function() return self:getOptionValue() and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER end } + text[#text + 1] = { tile = function() return self:getOptionValue() and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT end } + self:setText(text) end -local shortenings = { - ["Store item in stockpile"] = "Store item", +--- + +if RELOAD then config = nil end +config = config or { + follow_units = true, + follow_mouse = false, + show_happiness = true, + happiness_levels = { + -- keep in mind, the text will look differently with game's font + -- colors are same as in ASCII mode, but for then middle (3), which is GREY instead of WHITE + [0] = + {text = "=C", pen = COLOR_RED, visible = true, name = "Miserable"}, + {text = ":C", pen = COLOR_LIGHTRED, visible = true, name = "Unhappy"}, + {text = ":(", pen = COLOR_YELLOW, visible = false, name = "Displeased"}, + {text = ":]", pen = COLOR_GREY, visible = false, name = "Content"}, + {text = ":)", pen = COLOR_GREEN, visible = false, name = "Pleased"}, + {text = ":D", pen = COLOR_LIGHTGREEN, visible = true, name = "Happy"}, + {text = "=D", pen = COLOR_LIGHTCYAN, visible = true, name = "Ecstatic"}, + }, + show_unit_jobs = true, + job_shortenings = { + ["Store item in stockpile"] = "Store item", + } } -------------------------------------------------------------------------------- @@ -53,44 +113,93 @@ TooltipControlWindow.ATTRS { frame_inset=0, resizable=false, frame = { - w = 25, - h = 4, + w = 27, + h = 2 -- border + + 4 -- main options + + 7 -- happiness + , -- just under the minimap: r = 2, t = 18, }, } +-- right pad string `s` to `n` symbols with spaces +local function rpad(s, n) + local formatStr = "%-" .. n .. "s" -- `"%-10s"` + return string.format(formatStr, s) +end + function TooltipControlWindow:init() + local w = self.frame.w - 2 - 3 -- 2 is border, 3 is active indicator width + local keyW = 7 -- Length of "Alt+u: " + self:addviews{ - widgets.ToggleHotkeyLabel{ + ToggleLabel{ view_id = 'btn_follow_units', frame={t=0, h=1}, - label="Follow units", + label=rpad("Unit banners", w - keyW), key='CUSTOM_ALT_U', - on_change=change_follow_units, + initial_option=config.follow_units, + on_change=function(new) config.follow_units = new end, }, - widgets.ToggleHotkeyLabel{ + ToggleLabel{ view_id = 'btn_follow_mouse', frame={t=1, h=1}, - label="Follow mouse", + label=rpad("Mouse tooltip", w - keyW), key='CUSTOM_ALT_M', - on_change=change_follow_mouse, + initial_option=config.follow_mouse, + on_change=function(new) config.follow_mouse = new end, + }, + ToggleLabel{ + frame={t=2, h=1}, + label=rpad("Show jobs", w), + initial_option=config.show_unit_jobs, + on_change=function(new) config.show_unit_jobs = new end, + }, + ToggleLabel{ + frame={t=3, h=1}, + label=rpad("Show stress levels", w), + initial_option=config.show_happiness, + on_change=function(new) config.show_happiness = new end, }, } + + local happinessLabels = {} + + -- align the emoticons + local maxNameLength = 1 + for _, v in pairs(config.happiness_levels) do + local l = #v.name + if l > maxNameLength then + maxNameLength = l + end + end + + local indent = 3 + for lvl, cfg in pairs(config.happiness_levels) do + happinessLabels[#happinessLabels + 1] = ToggleLabel{ + frame={t=4+lvl, h=1, l=indent}, + initial_option=cfg.visible, + text_pen = cfg.pen, + label = rpad(rpad(cfg.name, maxNameLength) .. " " .. cfg.text, w - indent), + on_change = function(new) cfg.visible = new end + } + end + self:addviews(happinessLabels) end local function GetUnitHappiness(unit) - -- keep in mind, this will look differently with game's font - local mapToEmoticon = {[0] = "=C", ":C", ":(", ":]", ":)", ":D", "=D" } - -- same as in ASCII mode, but for then middle (3), which is GREY instead of WHITE - local mapToColor = {[0] = COLOR_RED, COLOR_LIGHTRED, COLOR_YELLOW, COLOR_GREY, COLOR_GREEN, COLOR_LIGHTGREEN, COLOR_LIGHTCYAN} + if not config.show_happiness then return end local stressCat = dfhack.units.getStressCategory(unit) if stressCat > 6 then stressCat = 6 end - return mapToEmoticon[stressCat], mapToColor[stressCat] + local happiness_level_cfg = config.happiness_levels[stressCat] + if not happiness_level_cfg.visible then return end + return happiness_level_cfg.text, happiness_level_cfg.pen end local function GetUnitJob(unit) + if not config.show_unit_jobs then return end local job = unit.job.current_job return job and dfhack.job.getName(job) end @@ -189,7 +298,6 @@ end -- map coordinates -> interface layer coordinates local function GetScreenCoordinates(map_coord) - if not map_coord then return end -- -> map viewport offset local vp = df.global.world.viewport local vp_Coord = vp.corner @@ -234,12 +342,13 @@ function TooltipsOverlay:render(dc) local height = vp.max_y local bottomright = {x = topleft.x + width, y = topleft.y + height, z = topleft.z} - local units = dfhack.units.getUnitsInBox(topleft, bottomright) or {} - if #units == 0 then return end + local units = dfhack.units.getUnitsInBox(topleft, bottomright) + if not units or #units == 0 then return end local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) local pen = COLOR_WHITE + local shortenings = config.job_shortenings local used_tiles = {} for i = #units, 1, -1 do local unit = units[i] @@ -252,7 +361,9 @@ function TooltipsOverlay:render(dc) local pos = xyz2pos(dfhack.units.getPosition(unit)) if not pos then goto continue end - local txt = table.concat({happiness, job}, " ") + local txt = (happiness and job and happiness .. " " .. job) + or happiness + or job local scrPos = GetScreenCoordinates(pos) local y = scrPos.y - 1 -- subtract 1 to move the text over the heads From e97201c1b1ea2bcb2637ad01ee3705afc2c4e831 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 18 Jan 2025 23:46:28 +0100 Subject: [PATCH 330/811] trim trailing whitespaces --- gui/tooltips.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index cf1b861f82..fbc52fdb14 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -175,7 +175,7 @@ function TooltipControlWindow:init() maxNameLength = l end end - + local indent = 3 for lvl, cfg in pairs(config.happiness_levels) do happinessLabels[#happinessLabels + 1] = ToggleLabel{ From 2e1cf878b24b012e49468595e539853c02584a6e Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 18 Jan 2025 23:57:16 +0100 Subject: [PATCH 331/811] fix an ASCII mode exception --- gui/tooltips.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index fbc52fdb14..31eee1510f 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -216,6 +216,8 @@ local function GetUnitNameAndJob(unit) end local function GetTooltipText(pos) + if not pos then return end + local txt = {} local units = dfhack.units.getUnitsInBox(pos, pos) or {} -- todo: maybe (optionally) use filter parameter here? @@ -267,7 +269,7 @@ function MouseTooltip:render(dc) local pos = dfhack.gui.getMousePos() local text = GetTooltipText(pos) - if #text == 0 then return end + if not text or #text == 0 then return end self.label:setText(text) local sw, sh = dfhack.screen.getWindowSize() From be0ffee4b1aa64152632f5664aa296886584825d Mon Sep 17 00:00:00 2001 From: Bjorn Macintosh Date: Sun, 19 Jan 2025 10:53:55 +1000 Subject: [PATCH 332/811] Feat: Add "show" to assign-preferences script This feature returns the list of preferences for a selected dwarf. This script may be useful if: * a user wants to view a quick list of likes for a dwarf * a user wants to view the list of likes after updating a dwarfs likes * Other scripts may find this helpful, as this functionality is not otherwise exposed via dfhack --- assign-preferences.lua | 236 ++++++++---------------------------- changelog.txt | 1 + docs/assign-preferences.rst | 4 +- 3 files changed, 57 insertions(+), 184 deletions(-) diff --git a/assign-preferences.lua b/assign-preferences.lua index ba791278f2..30603cc1ac 100644 --- a/assign-preferences.lua +++ b/assign-preferences.lua @@ -1,193 +1,12 @@ -- Change the preferences of a unit. --@ module = true -local help = [====[ - -assign-preferences -================== -A script to change the preferences of a unit. - -Preferences are classified into 12 types. The first 9 are: - -* like material; -* like creature; -* like food; -* hate creature; -* like item; -* like plant; -* like tree; -* like colour; -* like shape. - -These can be changed using this script. - -The remaining three are not currently managed by this script, -and are: like poetic form, like musical form, like dance form. - -To produce the correct description in the "thoughts and preferences" -page, you must specify the particular type of preference. For -each type, a description is provided in the section below. - -You will need to know the token of the object you want your dwarf to like. -You can find them in the wiki, otherwise in the folder "/raw/objects/" under -the main DF directory you will find all the raws defined in the game. - -For more information: -https://dwarffortresswiki.org/index.php/DF2014:Preferences - -Usage: - -``-help``: - print the help page. - -``-unit ``: - set the target unit ID. If not present, the - currently selected unit will be the target. - -``-likematerial [ <...> ]``: - usually a type of stone, a type of metal and a type - of gem, plus it can also be a type of wood, a type of - glass, a type of leather, a type of horn, a type of - pearl, a type of ivory, a decoration material - coral - or amber, a type of bone, a type of shell, a type - of silk, a type of yarn, or a type of plant cloth. - Write the full tokens. - There must be a space before and after each square - bracket. - -``-likecreature [ <...> ]``: - one or more creatures liked by the unit. You can - just list the species: the creature token will be - something similar to ``CREATURE:SPARROW:SKIN``, - so the name of the species will be ``SPARROW``. Nothing - will stop you to write the full token, if you want: the - script will just ignore the first and the last parts. - There must be a space before and after each square - bracket. - -``-likefood [ <...> ]``: - usually a type of alcohol, plus it can be a type of - meat, a type of fish, a type of cheese, a type of edible - plant, a cookable plant/creature extract, a cookable - mill powder, a cookable plant seed or a cookable plant - leaf. Write the full tokens. - There must be a space before and after each square - bracket. - -``-hatecreature [ <...> ]``: - works the same way as ``-likecreature``, but this time - it's one or more creatures that the unit detests. They - should be a type of ``HATEABLE`` vermin which isn't already - explicitly liked, but no check is performed about this. - Like before, you can just list the creature species. - There must be a space before and after each square - bracket. - -``-likeitem [ <...> ]``: - a kind of weapon, a kind of ammo, a kind of piece of - armor, a piece of clothing (including backpacks or - quivers), a type of furniture (doors, floodgates, beds, - chairs, windows, cages, barrels, tables, coffins, - statues, boxes, armor stands, weapon racks, cabinets, - bins, hatch covers, grates, querns, millstones, traction - benches, or slabs), a kind of craft (figurines, amulets, - scepters, crowns, rings, earrings, bracelets, or large - gems), or a kind of miscellaneous item (catapult parts, - ballista parts, a type of siege ammo, a trap component, - coins, anvils, totems, chains, flasks, goblets, - buckets, animal traps, an instrument, a toy, splints, - crutches, or a tool). The item tokens can be found here: - https://dwarffortresswiki.org/index.php/DF2014:Item_token - If you want to specify an item subtype, look into the files - listed under the column "Subtype" of the wiki page (they are - in the "/raw/ojects/" folder), then specify the items using - the full tokens found in those files (see examples below). - There must be a space before and after each square - bracket. - -``-likeplant [ <...> ]``: - works in a similar way as ``-likecreature``, this time - with plants. You can just List the plant species (the - middle part of the token). - There must be a space before and after each square - bracket. - -``-liketree [ <...> ]``: - works exactly as ``-likeplant``. I think this - preference type is here for backward compatibility (?). - You can still use it, however. As before, - you can just list the tree (plant) species. - There must be a space before and after each square - bracket. - -``-likecolor [ <...> ]``: - you can find the color tokens here: - https://dwarffortresswiki.org/index.php/DF2014:Color#Color_tokens - or inside the "descriptor_color_standard.txt" file - (in the "/raw/ojects/" folder). You can use the full token or - just the color name. - There must be a space before and after each square - bracket. - -``-likeshape [ <...> ]``: - I couldn't find a list of shape tokens in the wiki, but you - can find them inside the "descriptor_shape_standard.txt" - file (in the "/raw/ojects/" folder). You can - use the full token or just the shape name. - There must be a space before and after each square - bracket. - -``-reset``: - clear all preferences. If the script is called - with both this option and one or more preferences, - first all the unit preferences will be cleared - and then the listed preferences will be added. - -Examples: - -* "likes alabaster and willow wood":: - - assign-preferences -reset -likematerial [ INORGANIC:ALABASTER PLANT:WILLOW:WOOD ] - -* "likes sparrows for their ...":: - - assign-preferences -reset -likecreature SPARROW - -* "prefers to consume dwarven wine and olives":: - - assign-preferences -reset -likefood [ PLANT:MUSHROOM_HELMET_PLUMP:DRINK PLANT:OLIVE:FRUIT ] - -* "absolutely detests jumping spiders:: - - assign-preferences -reset -hatecreature SPIDER_JUMPING - -* "likes logs and battle axes":: - - assign-preferences -reset -likeitem [ WOOD ITEM_WEAPON:ITEM_WEAPON_AXE_BATTLE ] - -* "likes straberry plants for their ...":: - - assign-preferences -reset -likeplant BERRIES_STRAW - -* "likes oaks for their ...":: - - assign-preferences -reset -liketree OAK - -* "likes the color aqua":: - - assign-preferences -reset -likecolor AQUA - -* "likes stars":: - - assign-preferences -reset -likeshape STAR - -]====] - local utils = require("utils") local valid_args = utils.invert({ 'help', 'unit', + 'show', 'likematerial', 'likecreature', 'likefood', @@ -207,6 +26,38 @@ local function print_yellow(text) dfhack.color(-1) end +local function format_preference(pref, index) + print(string.format("Preference #%d:", index)) + + local pref_type = df.unit_preference.T_type[pref.type] + local description = "" + if pref_type == "LikeMaterial" then + description = "Likes material: " .. dfhack.matinfo.getToken(pref.mattype, pref.matindex) + elseif pref_type == "LikeFood" then + description = "Likes food: " .. dfhack.matinfo.getToken(pref.mattype, pref.matindex) + elseif pref_type == "LikeItem" then + description = "Likes item type: " .. tostring(pref.item_type) + elseif pref_type == "LikePlant" then + description = "Likes plant: " .. dfhack.matinfo.getToken(pref.mattype, pref.matindex) + elseif pref_type == "HateCreature" then + description = "Hates creature: " .. df.global.world.raws.creatures.all[pref.creature_id].creature_id + elseif pref_type == "LikeColor" then + description = "Likes color: " .. df.global.world.raws.descriptors.colors[pref.color_id].id + elseif pref_type == "LikeShape" then + description = "Likes shape: " .. df.global.world.raws.descriptors.shapes[pref.shape_id].id + elseif pref_type == "LikePoeticForm" then + description = "Likes poetic form: " .. dfhack.translation.translateName(df.global.world.poetic_forms.all[pref.poetic_form_id].name, true) + elseif pref_type == "LikeMusicalForm" then + description = "Likes musical form: " .. dfhack.translation.translateName(df.global.world.musical_forms.all[pref.musical_form_id].name, true) + elseif pref_type == "LikeDanceForm" then + description = "Likes dance form: " .. dfhack.translation.translateName(df.global.world.dance_forms.all[pref.dance_form_id].name, true) + else + description = "Unknown preference type: " .. tostring(pref.type) + end + + print(description) +end + -- initialise random number generator local rng = dfhack.random.new() @@ -624,12 +475,25 @@ function assign(preferences, unit, reset) end end +-- ----------------------------------------------- SHOW PREF UTILITY ------------------------------------------------ -- +local function showPreferences(unit) + assert(not unit or type(unit) == "number" or df.unit:is_instance(unit)) + unit = unit or dfhack.gui.getSelectedUnit(true) + if not unit then + qerror("No unit found.") + end + + for i, pref in ipairs(unit.status.current_soul.preferences) do + format_preference(pref, i) + end +end + -- ------------------------------------------------------ MAIN ------------------------------------------------------ -- local function main(...) local args = utils.processArgs({ ... }, valid_args) if args.help then - print(help) + print(dfhack.script_help()) return end @@ -646,9 +510,15 @@ local function main(...) reset = true end + if args.show then + showPreferences(unit) + return + end + -- parse preferences args.unit = nil -- remove from args table args.reset = nil -- remove from args table + args.show = nil -- remove from args table local preferences = {} utils.assign(preferences, args) diff --git a/changelog.txt b/changelog.txt index e8ad3bfc33..e63284553e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -61,6 +61,7 @@ Template for new versions: - `caravan`: add filter for written works in display furniture assignment dialog - `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) - `gui/sitemap`: show primary group affiliation for visitors and invaders (e.g. civilization name or performance troupe) +- `assign-preferences`: updated to allow users to run `assign-preferences -show` to get a list of selected units preferences # 50.15-r1 diff --git a/docs/assign-preferences.rst b/docs/assign-preferences.rst index 1345b1d262..441452ff37 100644 --- a/docs/assign-preferences.rst +++ b/docs/assign-preferences.rst @@ -2,7 +2,7 @@ assign-preferences ================== .. dfhack-tool:: - :summary: Adjust a unit's preferences. + :summary: View or Adjust a unit's preferences. :tags: fort armok units You will need to know the token of the object you want your dwarf to like. @@ -71,6 +71,8 @@ brackets can be omitted. ``--unit `` The target unit ID. If not present, the currently selected unit will be the target. +``--show`` + Print the list of current likes/dislikes for the selected unit ``--likematerial [ [ ...] ]`` This is usually set to three tokens: a type of stone, a type of metal, and a type of gem. It can also be a type of wood, glass, leather, horn, pearl, From 1ae2c74d27e97ff396d31a78c8eee35997a62b5d Mon Sep 17 00:00:00 2001 From: TolMera Date: Sun, 19 Jan 2025 18:50:48 +1000 Subject: [PATCH 333/811] Update pref-adjust.lua otherwise you'll get an extra error printed to the console when no unit is selected Co-authored-by: Myk --- pref-adjust.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pref-adjust.lua b/pref-adjust.lua index b94f709c7a..93ea84d6b5 100644 --- a/pref-adjust.lua +++ b/pref-adjust.lua @@ -362,7 +362,7 @@ elseif opt == "all" then elseif opt == "goth_all" then handle_all("GOTH") if opt == "show" then - local unit = dfhack.gui.getSelectedUnit() + local unit = dfhack.gui.getSelectedUnit(true) get_preferences(unit) else print ("Sets preferences of one dwarf, or of all dwarves, using profiles.") From 1972994330853ebaac849042a34836b6b4796a42 Mon Sep 17 00:00:00 2001 From: TolMera Date: Sun, 19 Jan 2025 18:55:40 +1000 Subject: [PATCH 334/811] fix: Changes inline with PR feedback --- pref-adjust.lua | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/pref-adjust.lua b/pref-adjust.lua index 93ea84d6b5..95514b0cca 100644 --- a/pref-adjust.lua +++ b/pref-adjust.lua @@ -269,7 +269,8 @@ function build_all_lists(printflag) end -- end func build_all_lists -- --------------------------------------------------------------------------- function get_preferences(unit) - if unit == nil then + if not unit then + print("No unit selected!") return end @@ -294,17 +295,17 @@ function get_preferences(unit) elseif pref_type == "LikePlant" then description = "Likes plant: " .. dfhack.matinfo.getToken(pref.mattype, pref.matindex) elseif pref_type == "HateCreature" then - description = "Hates creature: " .. df.global.world.raws.creatures.all[pref.poetic_form_id].creature_id + description = "Hates creature: " .. df.global.world.raws.creatures.all[pref.creature_id].creature_id elseif pref_type == "LikeColor" then - description = "Likes color: " .. df.global.world.raws.descriptors.colors[pref.poetic_form_id].id + description = "Likes color: " .. df.global.world.raws.descriptors.colors[pref.color_id].id elseif pref_type == "LikeShape" then - description = "Likes shape: " .. df.global.world.raws.descriptors.shapes[pref.poetic_form_id].id + description = "Likes shape: " .. df.global.world.raws.descriptors.shapes[pref.shape_id].id elseif pref_type == "LikePoeticForm" then description = "Likes poetic form: " .. dfhack.translation.translateName(df.global.world.poetic_forms.all[pref.poetic_form_id].name, true) elseif pref_type == "LikeMusicalForm" then - description = "Likes musical form: " .. dfhack.translation.translateName(df.global.world.musical_forms.all[pref.poetic_form_id].name, true) + description = "Likes musical form: " .. dfhack.translation.translateName(df.global.world.musical_forms.all[pref.musical_form_id].name, true) elseif pref_type == "LikeDanceForm" then - description = "Likes dance form: " .. dfhack.translation.translateName(df.global.world.dance_forms.all[pref.poetic_form_id].name, true) + description = "Likes dance form: " .. dfhack.translation.translateName(df.global.world.dance_forms.all[pref.dance_form_id].name, true) else description = "Unknown preference type: " .. tostring(pref.type) end From 3c64af684c6a7a7615a5417174f33fe5735272d9 Mon Sep 17 00:00:00 2001 From: TolMera Date: Sun, 19 Jan 2025 19:18:39 +1000 Subject: [PATCH 335/811] Doc: update documentation Functional update: updated script to use call to `dfhack.script_help` function --- docs/pref-adjust.rst | 16 ++++++++++------ pref-adjust.lua | 14 ++------------ 2 files changed, 12 insertions(+), 18 deletions(-) diff --git a/docs/pref-adjust.rst b/docs/pref-adjust.rst index a6794319eb..1fb42a475e 100644 --- a/docs/pref-adjust.rst +++ b/docs/pref-adjust.rst @@ -2,8 +2,8 @@ pref-adjust =========== .. dfhack-tool:: - :summary: Set the preferences of a dwarf to an ideal. - :tags: fort armok units + :summary: Get/Set the preferences of a dwarf. + :tags: fort armok units preferences This tool replaces a dwarf's preferences with an "ideal" set which is easy to satisfy:: @@ -16,19 +16,21 @@ satisfy:: Usage ----- +``pref-adjust list`` + List all types of preferences. No changes will be made to any units. +``pref-adjust show`` + Show the preferences of a unit. ``pref-adjust all|goth_all|clear_all`` - Changes/clears preferences for all dwarves. + Changes/clears preferences for all units. ``pref-adjust one|goth|clear`` Changes/clears preferences for the currently selected dwarf. -``pref-adjust list`` - List all types of preferences. No changes will be made to any dwarves. Examples -------- ``pref-adjust all`` - Change preferences for all dwarves to an ideal. + Change preferences for all units to an ideal. Goth mode --------- @@ -41,3 +43,5 @@ instead of the easy-to-satisfy ideal defaults:: their horrifying features. When possible, she prefers to consume sewer brew, gutter cruor and bloated tubers. She absolutely detests elves, humans and dwarves. + + diff --git a/pref-adjust.lua b/pref-adjust.lua index 95514b0cca..0c6e1bac73 100644 --- a/pref-adjust.lua +++ b/pref-adjust.lua @@ -270,7 +270,6 @@ end -- end func build_all_lists -- --------------------------------------------------------------------------- function get_preferences(unit) if not unit then - print("No unit selected!") return end @@ -362,20 +361,11 @@ elseif opt == "all" then handle_all("IDEAL") elseif opt == "goth_all" then handle_all("GOTH") -if opt == "show" then +elseif opt == "show" then local unit = dfhack.gui.getSelectedUnit(true) get_preferences(unit) else - print ("Sets preferences of one dwarf, or of all dwarves, using profiles.") - print ("Valid options:") - print ("list -- show available preference type lists") - print ("show -- show current preferences") - print ("clear -- clear preferences of selected unit") - print ("clear_all -- clear preferences of all units") - print ("goth -- alter current dwarf preferences to Goth") - print ("goth_all -- alter all dwarf preferences to Goth") - print ("one -- alter current dwarf preferences to Ideal") - print ("all -- alter all dwarf preferences to Ideal") + print(dfhack.script_help()) if opt and opt ~= "help" then qerror("Unrecognized option: " .. opt) end From e42ad6e4699d41f70cbe50570f7280b6912615b5 Mon Sep 17 00:00:00 2001 From: TolMera Date: Sun, 19 Jan 2025 19:23:11 +1000 Subject: [PATCH 336/811] Doc: updated changelog Added notes to future>misc as requested in PR --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 45dacce451..151c2bcddd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -62,6 +62,7 @@ Template for new versions: - `caravan`: add filter for written works in display furniture assignment dialog - `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) - `gui/sitemap`: show primary group affiliation for visitors and invaders (e.g. civilization name or performance troupe) +- `pref-assign`: updated to allow users to run `pref-assign show` to get a list of units current preferences # 50.15-r1 From 6609afa18330fc7554a22286ff091cf5fbb10699 Mon Sep 17 00:00:00 2001 From: TolMera Date: Sun, 19 Jan 2025 19:26:06 +1000 Subject: [PATCH 337/811] Fix: Fix calls to `getSelectedUnit` add arg `true` Feedback on this is that if a unit is not selected, and this function is called, you will get two errors printed instead of the expected one. --- pref-adjust.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pref-adjust.lua b/pref-adjust.lua index 0c6e1bac73..e7eb3af079 100644 --- a/pref-adjust.lua +++ b/pref-adjust.lua @@ -326,7 +326,7 @@ build_all_lists(false) local opt = ({...})[1] function handle_one(profile) - local unit = dfhack.gui.getSelectedUnit() + local unit = dfhack.gui.getSelectedUnit(true) if unit == nil then print ("No unit available! Aborting with extreme prejudice.") return @@ -343,7 +343,7 @@ end if opt == "list" then build_all_lists(true) elseif opt == "clear" then - local unit = dfhack.gui.getSelectedUnit() + local unit = dfhack.gui.getSelectedUnit(true) if unit==nil then print ("No unit available! Aborting with extreme prejudice.") return From 76a25764ffd0c3e289134c8f6f37705afafbaf23 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 19 Jan 2025 09:29:26 +0000 Subject: [PATCH 338/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/pref-adjust.rst | 2 -- pref-adjust.lua | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/docs/pref-adjust.rst b/docs/pref-adjust.rst index 1fb42a475e..5299772427 100644 --- a/docs/pref-adjust.rst +++ b/docs/pref-adjust.rst @@ -43,5 +43,3 @@ instead of the easy-to-satisfy ideal defaults:: their horrifying features. When possible, she prefers to consume sewer brew, gutter cruor and bloated tubers. She absolutely detests elves, humans and dwarves. - - diff --git a/pref-adjust.lua b/pref-adjust.lua index e7eb3af079..b06804f6cf 100644 --- a/pref-adjust.lua +++ b/pref-adjust.lua @@ -269,7 +269,7 @@ function build_all_lists(printflag) end -- end func build_all_lists -- --------------------------------------------------------------------------- function get_preferences(unit) - if not unit then + if not unit then print("No unit selected!") return end From 868995e93fbf77a13e8e992f7965d845a235e1f1 Mon Sep 17 00:00:00 2001 From: TolMera Date: Sun, 19 Jan 2025 20:11:04 +1000 Subject: [PATCH 339/811] Doc: clarified documentation summary of `pref-adjust` --- docs/pref-adjust.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pref-adjust.rst b/docs/pref-adjust.rst index 5299772427..2c0330b079 100644 --- a/docs/pref-adjust.rst +++ b/docs/pref-adjust.rst @@ -2,7 +2,7 @@ pref-adjust =========== .. dfhack-tool:: - :summary: Get/Set the preferences of a dwarf. + :summary: Get the preferences of a dwarf, Set the preferences of a dwarf to a designated profile. :tags: fort armok units preferences This tool replaces a dwarf's preferences with an "ideal" set which is easy to From 4b4c595d8f7fb5403411645aebe3ecac87e5b86e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 19 Jan 2025 10:56:32 +0000 Subject: [PATCH 340/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- assign-preferences.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/assign-preferences.lua b/assign-preferences.lua index 30603cc1ac..dae521d379 100644 --- a/assign-preferences.lua +++ b/assign-preferences.lua @@ -482,7 +482,7 @@ local function showPreferences(unit) if not unit then qerror("No unit found.") end - + for i, pref in ipairs(unit.status.current_soul.preferences) do format_preference(pref, i) end From 735f5d5a25d5cce7e6575d69aa6ed745bcad1ecd Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:15:32 -0600 Subject: [PATCH 341/811] Clean up code Move Slider example into the hello world move the Slider widget away to its proper place (DFHack Repo). --- devel/hello-world.lua | 33 ++++++++++- devel/helloSlider.lua | 71 ----------------------- devel/slider.lua | 132 ------------------------------------------ 3 files changed, 32 insertions(+), 204 deletions(-) delete mode 100644 devel/helloSlider.lua delete mode 100644 devel/slider.lua diff --git a/devel/hello-world.lua b/devel/hello-world.lua index 679bf1d52e..bf872f4baa 100644 --- a/devel/hello-world.lua +++ b/devel/hello-world.lua @@ -12,13 +12,23 @@ local HIGHLIGHT_PEN = dfhack.pen.parse{ HelloWorldWindow = defclass(HelloWorldWindow, widgets.Window) HelloWorldWindow.ATTRS{ - frame={w=20, h=14}, + frame={w=25, h=20}, frame_title='Hello World', autoarrange_subviews=true, autoarrange_gap=1, + resizable=true, + resize_min={w=25, h=20}, } function HelloWorldWindow:init() + local LEVEL_OPTIONS = { + {label='Low', value=1}, + {label='Medium', value=2}, + {label='High', value=3}, + {label='Pro', value=4}, + {label='Insane', value=5}, + } + self:addviews{ widgets.Label{text={{text='Hello, world!', pen=COLOR_LIGHTGREEN}}}, widgets.HotkeyLabel{ @@ -32,6 +42,27 @@ function HelloWorldWindow:init() frame={w=10, h=5}, frame_style=gui.INTERIOR_FRAME, }, + widgets.CycleHotkeyLabel{ + view_id='level', + frame={l=1, t=0, w=16}, + label='Level:', + label_below=true, + key_back='CUSTOM_SHIFT_C', + key='CUSTOM_SHIFT_V', + options=LEVEL_OPTIONS, + initial_option=LEVEL_OPTIONS[1].value, + on_change=function(val) + self.subviews.level:setOption(val) + end, + }, + widgets.Slider{ + frame={l=1, t=3}, + num_stops=#LEVEL_OPTIONS, + get_idx_fn=function() + return self.subviews.level:getOptionValue() + end, + on_change=function(idx) self.subviews.level:setOption(idx) end, + }, } end diff --git a/devel/helloSlider.lua b/devel/helloSlider.lua deleted file mode 100644 index e2e250df6b..0000000000 --- a/devel/helloSlider.lua +++ /dev/null @@ -1,71 +0,0 @@ -local gui = require('gui') -local widgets = require('gui.widgets') - --- --- RangerWindow --- - -RangerWindow = defclass(RangerWindow, widgets.Window) -RangerWindow.ATTRS { - frame_title='Hello, Slider!', - frame={w=25, h=8}, - resizable=true, - resize_min={w=25, h=8}, -} - -function RangerWindow:init() - local LEVEL_OPTIONS = { - {label='Low', value=1}, - {label='Medium', value=2}, - {label='High', value=3}, - {label='Pro', value=4}, - {label='Insane', value=5}, - } - - self:addviews{ - widgets.CycleHotkeyLabel{ - view_id='level', - frame={l=1, t=0, w=16}, - label='Level:', - label_below=true, - key_back='CUSTOM_SHIFT_C', - key='CUSTOM_SHIFT_V', - options=LEVEL_OPTIONS, - initial_option=LEVEL_OPTIONS[1].value, - on_change=function(val) - self.subviews.level:setOption(val) - end, - }, - widgets.Slider{ - frame={l=1, t=3}, - num_stops=#LEVEL_OPTIONS, - get_idx_fn=function() - return self.subviews.level:getOptionValue() - end, - on_change=function(idx) self.subviews.level:setOption(idx) end, - }, - } -end - --- --- RangerScreen --- - -RangerScreen = defclass(RangerScreen, gui.ZScreen) -RangerScreen.ATTRS { - focus_path='ranger', -} - -function RangerScreen:init() - self:addviews{RangerWindow{}} -end - -function RangerScreen:onDismiss() - view = nil -end - --- --- main logic --- - -view = view and view:raise() or RangerScreen{}:show() diff --git a/devel/slider.lua b/devel/slider.lua deleted file mode 100644 index 92263d2924..0000000000 --- a/devel/slider.lua +++ /dev/null @@ -1,132 +0,0 @@ -local Widget = require('gui.widgets.widget') - -local to_pen = dfhack.pen.parse - --------------------------------- --- Slider --------------------------------- - ----@class widgets.Slider.attrs: widgets.Widget.attrs ----@field num_stops integer ----@field get_idx_fn? function ----@field on_change? fun(index: integer) - ----@class widgets.Slider.attrs.partial: widgets.Slider.attrs - ----@class widgets.Slider.initTable: widgets.Slider.attrs ----@field num_stops integer - ----@class widgets.Slider: widgets.Widget, widgets.Slider.attrs ----@field super widgets.Widget ----@field ATTRS widgets.Slider.attrs|fun(attributes: widgets.Slider.attrs.partial) ----@overload fun(init_table: widgets.Slider.initTable): self -Slider = defclass(Slider, Widget) -Slider.ATTRS{ - num_stops=DEFAULT_NIL, - get_idx_fn=DEFAULT_NIL, - on_change=DEFAULT_NIL, -} - -function Slider:preinit(init_table) - init_table.frame = init_table.frame or {} - init_table.frame.h = init_table.frame.h or 1 -end - -function Slider:init() - if self.num_stops < 2 then error('too few Slider stops') end - self.is_dragging_target = nil -- 'left', 'right', or 'both' - self.is_dragging_idx = nil -- offset from leftmost dragged tile -end - -local function Slider_get_width_per_idx(self) - return math.max(3, (self.frame_body.width-7) // (self.num_stops-1)) -end - -function Slider:onInput(keys) - if not keys._MOUSE_L then return false end - local x = self:getMousePos() - if not x then return false end - local left_idx = self.get_idx_fn() - local width_per_idx = Slider_get_width_per_idx(self) - local left_pos = width_per_idx*(left_idx-1) - local right_pos = width_per_idx*(left_idx-1) + 4 - if x < left_pos then - self.on_change(self.get_idx_fn() - 1) - else - self.is_dragging_target = 'both' - self.is_dragging_idx = x - right_pos - end - return true -end - -local function Slider_do_drag(self, width_per_idx) - local x = self.frame_body:localXY(dfhack.screen.getMousePos()) - local cur_pos = x - self.is_dragging_idx - cur_pos = math.max(0, cur_pos) - cur_pos = math.min(width_per_idx*(self.num_stops-1)+7, cur_pos) - local offset = 1 - local new_idx = math.max(0, cur_pos+offset)//width_per_idx + 1 - if self.is_dragging_target == 'both' then - if new_idx > self.num_stops then - return - end - end - if new_idx and new_idx ~= self.get_idx_fn() then - self.on_change(new_idx) - end -end - -local SLIDER_LEFT_END = to_pen{ch=198, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK = to_pen{ch=205, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_SELECTED = to_pen{ch=205, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP = to_pen{ch=216, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TRACK_STOP_SELECTED = to_pen{ch=216, fg=COLOR_LIGHTGREEN, bg=COLOR_BLACK} -local SLIDER_RIGHT_END = to_pen{ch=181, fg=COLOR_GREY, bg=COLOR_BLACK} -local SLIDER_TAB_LEFT = to_pen{ch=60, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_CENTER = to_pen{ch=9, fg=COLOR_BLACK, bg=COLOR_YELLOW} -local SLIDER_TAB_RIGHT = to_pen{ch=62, fg=COLOR_BLACK, bg=COLOR_YELLOW} - -function Slider:onRenderBody(dc, rect) - local left_idx = self.get_idx_fn() - local width_per_idx = Slider_get_width_per_idx(self) - -- draw track - dc:seek(1,0) - dc:char(nil, SLIDER_LEFT_END) - dc:char(nil, SLIDER_TRACK) - for stop_idx=1,self.num_stops-1 do - local track_stop_pen = SLIDER_TRACK_STOP_SELECTED - local track_pen = SLIDER_TRACK_SELECTED - if left_idx ~= stop_idx then - track_stop_pen = SLIDER_TRACK_STOP - track_pen = SLIDER_TRACK - elseif left_idx == stop_idx then - track_pen = SLIDER_TRACK - end - dc:char(nil, track_stop_pen) - for i=2,width_per_idx do - dc:char(nil, track_pen) - end - end - if left_idx >= self.num_stops then - dc:char(nil, SLIDER_TRACK_STOP_SELECTED) - else - dc:char(nil, SLIDER_TRACK_STOP) - end - dc:char(nil, SLIDER_TRACK) - dc:char(nil, SLIDER_RIGHT_END) - -- draw tab - dc:seek(width_per_idx*(left_idx-1)+2) - dc:char(nil, SLIDER_TAB_LEFT) - dc:char(nil, SLIDER_TAB_CENTER) - dc:char(nil, SLIDER_TAB_RIGHT) - -- manage dragging - if self.is_dragging_target then - Slider_do_drag(self, width_per_idx) - end - if df.global.enabler.mouse_lbut_down == 0 then - self.is_dragging_target = nil - self.is_dragging_idx = nil - end -end - -return Slider From 9055dc8fe5a46e56786c7928493d7616593ee5ab Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Mon, 20 Jan 2025 12:34:33 -0600 Subject: [PATCH 342/811] Clean up the code and add a divider --- devel/hello-world.lua | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/devel/hello-world.lua b/devel/hello-world.lua index bf872f4baa..5e8b22ebb6 100644 --- a/devel/hello-world.lua +++ b/devel/hello-world.lua @@ -12,10 +12,10 @@ local HIGHLIGHT_PEN = dfhack.pen.parse{ HelloWorldWindow = defclass(HelloWorldWindow, widgets.Window) HelloWorldWindow.ATTRS{ - frame={w=25, h=20}, + frame={w=25, h=25}, frame_title='Hello World', autoarrange_subviews=true, - autoarrange_gap=1, + autoarrange_gap=2, resizable=true, resize_min={w=25, h=20}, } @@ -28,7 +28,7 @@ function HelloWorldWindow:init() {label='Pro', value=4}, {label='Insane', value=5}, } - + self:addviews{ widgets.Label{text={{text='Hello, world!', pen=COLOR_LIGHTGREEN}}}, widgets.HotkeyLabel{ @@ -42,9 +42,12 @@ function HelloWorldWindow:init() frame={w=10, h=5}, frame_style=gui.INTERIOR_FRAME, }, + widgets.Divider{ + frame={l=0,t=3} + }, widgets.CycleHotkeyLabel{ view_id='level', - frame={l=1, t=0, w=16}, + frame={l=0, t=3, w=16}, label='Level:', label_below=true, key_back='CUSTOM_SHIFT_C', From 82ea79581f5dd013114b28ae9631c2d57d90fc00 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 20 Jan 2025 19:32:54 -0800 Subject: [PATCH 343/811] edit autosave dialog text --- internal/notify/notifications.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index a527b43549..e992feb2e7 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -536,8 +536,8 @@ NOTIFICATIONS_BY_IDX = { end, on_click=function() local minsSinceSave = dfhack.persistent.getUnsavedSeconds()//60 - local message = 'It has been ' .. dfhack.formatInt(minsSinceSave) .. ' minutes since your last save. \n\nWould you like to save now? ' .. - '(Note: You can also close this reminder and save manually)' + local message = 'It has been ' .. dfhack.formatInt(minsSinceSave) .. ' minutes since your last save. \n\nWould you like to save now?\n\n' .. + 'You can also close this reminder and save manually.' dlg.showYesNoPrompt('Save now?', message, nil, function() dfhack.run_script('quicksave') end) end, }, From 6f71c2b27aa990fd650b71e6e5d5823ec208839f Mon Sep 17 00:00:00 2001 From: TolMera Date: Wed, 22 Jan 2025 03:25:02 +1000 Subject: [PATCH 344/811] Doc: fix punctuation Co-authored-by: Myk --- docs/assign-preferences.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/assign-preferences.rst b/docs/assign-preferences.rst index 441452ff37..ee77a22757 100644 --- a/docs/assign-preferences.rst +++ b/docs/assign-preferences.rst @@ -2,7 +2,7 @@ assign-preferences ================== .. dfhack-tool:: - :summary: View or Adjust a unit's preferences. + :summary: View or adjust a unit's preferences. :tags: fort armok units You will need to know the token of the object you want your dwarf to like. From f50291e588e5e7ded775c2bc610bf6687a62099e Mon Sep 17 00:00:00 2001 From: Bjorn Macintosh Date: Wed, 22 Jan 2025 03:26:37 +1000 Subject: [PATCH 345/811] Fix: incorrect location of changelog record --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 5c68a8dccd..6a7781d71f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: - `deathcause`: fix error when retrieving the name of a historical figure ## Misc Improvements +- `assign-preferences`: updated to allow users to run `assign-preferences -show` to get a list of selected units preferences ## Removed @@ -62,7 +63,6 @@ Template for new versions: - `caravan`: add filter for written works in display furniture assignment dialog - `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) - `gui/sitemap`: show primary group affiliation for visitors and invaders (e.g. civilization name or performance troupe) -- `assign-preferences`: updated to allow users to run `assign-preferences -show` to get a list of selected units preferences # 50.15-r1 From 78762a3a4917f0f2b511c183e490bb6e0a288f82 Mon Sep 17 00:00:00 2001 From: Bjorn Macintosh Date: Wed, 22 Jan 2025 03:27:50 +1000 Subject: [PATCH 346/811] Fix: instruction structure For notes see: https://github.com/DFHack/scripts/pull/1377#discussion_r1921540071 --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 6a7781d71f..7c032ffe63 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,7 +34,7 @@ Template for new versions: - `deathcause`: fix error when retrieving the name of a historical figure ## Misc Improvements -- `assign-preferences`: updated to allow users to run `assign-preferences -show` to get a list of selected units preferences +- `assign-preferences`: updated to allow users to run `assign-preferences --show` to get a list of selected units preferences ## Removed From 13cf421c1e2cb8ed5c938aa5c7532ee80d1bde66 Mon Sep 17 00:00:00 2001 From: TolMera Date: Wed, 22 Jan 2025 03:34:15 +1000 Subject: [PATCH 347/811] Doc: voice and tone of documentation adjustment Co-authored-by: Myk --- docs/pref-adjust.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pref-adjust.rst b/docs/pref-adjust.rst index 2c0330b079..cce5c47807 100644 --- a/docs/pref-adjust.rst +++ b/docs/pref-adjust.rst @@ -2,7 +2,7 @@ pref-adjust =========== .. dfhack-tool:: - :summary: Get the preferences of a dwarf, Set the preferences of a dwarf to a designated profile. + :summary: See the preferences of a dwarf or set them to a designated profile. :tags: fort armok units preferences This tool replaces a dwarf's preferences with an "ideal" set which is easy to From 529fbb47da83c1079e47b4664c292bf4c3193346 Mon Sep 17 00:00:00 2001 From: TolMera Date: Wed, 22 Jan 2025 03:36:36 +1000 Subject: [PATCH 348/811] Doc: voice and tone of documentation adjustment Co-authored-by: Myk --- docs/pref-adjust.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pref-adjust.rst b/docs/pref-adjust.rst index cce5c47807..e5fd02f3ac 100644 --- a/docs/pref-adjust.rst +++ b/docs/pref-adjust.rst @@ -3,7 +3,7 @@ pref-adjust .. dfhack-tool:: :summary: See the preferences of a dwarf or set them to a designated profile. - :tags: fort armok units preferences + :tags: fort armok units This tool replaces a dwarf's preferences with an "ideal" set which is easy to satisfy:: From b413d0e541dc1599251a1feab2cbb4941a2c9d41 Mon Sep 17 00:00:00 2001 From: TolMera Date: Wed, 22 Jan 2025 03:37:17 +1000 Subject: [PATCH 349/811] Doc: voice and tone of documentation adjustment Co-authored-by: Myk --- docs/pref-adjust.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pref-adjust.rst b/docs/pref-adjust.rst index e5fd02f3ac..99bb8df2fb 100644 --- a/docs/pref-adjust.rst +++ b/docs/pref-adjust.rst @@ -19,7 +19,7 @@ Usage ``pref-adjust list`` List all types of preferences. No changes will be made to any units. ``pref-adjust show`` - Show the preferences of a unit. + Show the preferences of the selected unit. ``pref-adjust all|goth_all|clear_all`` Changes/clears preferences for all units. ``pref-adjust one|goth|clear`` From 3ebe4307b179c777c8766aa63f8ba8a77e7b7363 Mon Sep 17 00:00:00 2001 From: TolMera Date: Wed, 22 Jan 2025 03:37:35 +1000 Subject: [PATCH 350/811] Doc: voice and tone of documentation adjustment Co-authored-by: Myk --- docs/pref-adjust.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pref-adjust.rst b/docs/pref-adjust.rst index 99bb8df2fb..61b8606a2e 100644 --- a/docs/pref-adjust.rst +++ b/docs/pref-adjust.rst @@ -30,7 +30,7 @@ Examples -------- ``pref-adjust all`` - Change preferences for all units to an ideal. + Change preferences for all citizens to an ideal. Goth mode --------- From 1da352a45c85135f6c188d46c3cc7dc93fe46b1b Mon Sep 17 00:00:00 2001 From: TolMera Date: Wed, 22 Jan 2025 03:37:52 +1000 Subject: [PATCH 351/811] Doc: voice and tone of documentation adjustment Co-authored-by: Myk --- docs/pref-adjust.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/pref-adjust.rst b/docs/pref-adjust.rst index 61b8606a2e..4a151cbb96 100644 --- a/docs/pref-adjust.rst +++ b/docs/pref-adjust.rst @@ -21,7 +21,7 @@ Usage ``pref-adjust show`` Show the preferences of the selected unit. ``pref-adjust all|goth_all|clear_all`` - Changes/clears preferences for all units. + Changes/clears preferences for all citizens. ``pref-adjust one|goth|clear`` Changes/clears preferences for the currently selected dwarf. From ca71d17ac1729d7548b7ded1093c0498c34abf8a Mon Sep 17 00:00:00 2001 From: Bjorn Macintosh Date: Wed, 22 Jan 2025 03:40:28 +1000 Subject: [PATCH 352/811] Fix: incorrect location of changelog record --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 151c2bcddd..65d3827fd0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: - `deathcause`: fix error when retrieving the name of a historical figure ## Misc Improvements +- `pref-assign`: updated to allow users to run `pref-assign show` to get a list of units current preferences ## Removed @@ -62,7 +63,6 @@ Template for new versions: - `caravan`: add filter for written works in display furniture assignment dialog - `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) - `gui/sitemap`: show primary group affiliation for visitors and invaders (e.g. civilization name or performance troupe) -- `pref-assign`: updated to allow users to run `pref-assign show` to get a list of units current preferences # 50.15-r1 From 67e94aa8160aca4a2beadaf628ea36bdf229eab0 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Tue, 21 Jan 2025 19:43:46 +0100 Subject: [PATCH 353/811] Stylistic improvements suggested by Myk Co-authored-by: Myk --- autocheese.lua | 4 ++-- docs/autocheese.rst | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/autocheese.lua b/autocheese.lua index fa01fc53d6..135e933feb 100644 --- a/autocheese.lua +++ b/autocheese.lua @@ -130,7 +130,7 @@ end local workshop = findWorkshop() if not workshop then - print('autocheese: no workshop available') + print('autocheese: no Farmer's Workshop available') return end @@ -142,7 +142,7 @@ end local job = makeCheese(reagent, workshop) print(('autocheese: dispatching cheesemaking job for %s (%d milk) to %s'):format( - dfhack.items.getDescription(reagent, 0), + dfhack.df2console(dfhack.items.getReadableDescription(reagent)), #reagent.general_refs, dfhack.df2console(dfhack.units.getReadableName(worker)) )) diff --git a/docs/autocheese.rst b/docs/autocheese.rst index b96ffd6510..fb92874df5 100644 --- a/docs/autocheese.rst +++ b/docs/autocheese.rst @@ -2,12 +2,13 @@ autocheese ========== .. dfhack-tool:: - :summary: Automatically make cheese using barrels that have accumulated sufficient milk. + :summary: Schedule cheese making jobs based on milk reserves. :tags: fort auto -Cheese making is difficult to automate using work orders, because a single job -can consume anything from a bucket was a single unit of milk to barrel -containing up to 100 units of milk. +Cheese making is difficult to automate using work orders. A single job +can consume anything from a bucket with a single unit of milk to a barrel +with 100 units of milk. This makes it hard to predict how much cheese will +actually be produced by an automated order. The script will scan your fort for barrels with a certain minimum amount of milk (default: 50), create a cheese making job specifically for that barrel, and From 44b5547b70f6734b67b22cb6d4e0e1a5deb4f999 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Tue, 21 Jan 2025 21:08:37 +0100 Subject: [PATCH 354/811] Implement feedback from code review - check that barrel can be brought to workshop - handle workshops with assigned masters --- autocheese.lua | 50 +++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 41 insertions(+), 9 deletions(-) diff --git a/autocheese.lua b/autocheese.lua index 135e933feb..b365f3f7b1 100644 --- a/autocheese.lua +++ b/autocheese.lua @@ -47,6 +47,17 @@ function unitIsAvailable(unit) return true end +---check if unit can perform labor at workshop +---@param unit df.unit +---@param unit_labor df.unit_labor +---@param workshop df.building +---@return boolean +function availableLaborer(unit, unit_labor, workshop) + return unit.status.labors[unit_labor] + and unitIsAvailable(unit) + and ic.canAccessWorkshop(unit, workshop) +end + ---find unit with a particular labor enabled ---@param unit_labor df.unit_labor ---@param job_skill df.job_skill @@ -58,9 +69,7 @@ end local max_skill = -1 for _, unit in ipairs(dfhack.units.getCitizens(true, false)) do if - unit.status.labors[unit_labor] and - unitIsAvailable(unit) and - ic.canAccessWorkshop(unit, workshop) + availableLaborer(unit, unit_labor, workshop) then local unit_skill = dfhack.units.getNominalSkill(unit, job_skill, true) if unit_skill > max_skill then @@ -90,13 +99,32 @@ local function findMilkBarrel(min_liquids) end end -function findWorkshop() +---find a workshop to which the barrel can be brought +---if the workshop has a master, only return workshop and master if the master is available +---@param pos df.coord +---@return df.building_workshopst? +---@return df.unit? +function findWorkshop(pos) for _,workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do if + dfhack.maps.canWalkBetween(pos, xyz2pos(workshop.centerx, workshop.centery, workshop.z)) and not workshop.profile.blocked_labors[df.unit_labor.MAKE_CHEESE] and - #workshop.jobs == 0 and #workshop.profile.permitted_workers == 0 + #workshop.jobs == 0 then - return workshop + if #workshop.profile.permitted_workers == 0 then + -- immediately return workshop without master + return workshop, nil + else + unit = df.unit.find(workshop.profile.permitted_workers[0]) + if + unit and availableLaborer(unit, df.unit_labor.MAKE_CHEESE, workshop) + then + -- return workshop and master, if master is available + return workshop, unit + else + print("autocheese: Skipping farmer's workshop with unavailable master") + end + end end end end @@ -127,14 +155,18 @@ if not reagent then return end -local workshop = findWorkshop() +local workshop, worker = findWorkshop(xyz2pos(dfhack.items.getPosition(reagent))) if not workshop then - print('autocheese: no Farmer's Workshop available') + print("autocheese: no Farmer's Workshop available") return end -local worker, skill = findAvailableLaborer(df.unit_labor.MAKE_CHEESE, df.job_skill.CHEESEMAKING, workshop) +-- try to find laborer for workshop without master +if not worker then + worker, _ = findAvailableLaborer(df.unit_labor.MAKE_CHEESE, df.job_skill.CHEESEMAKING, workshop) +end + if not worker then print('autocheese: no cheesemaker available') return From 34012d635d47da217e42e99aba1fba1c3690f665 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 23 Jan 2025 07:21:58 -0800 Subject: [PATCH 355/811] bump changelog --- changelog.txt | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 45dacce451..99313d061e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,12 +31,16 @@ Template for new versions: ## New Features ## Fixes -- `deathcause`: fix error when retrieving the name of a historical figure ## Misc Improvements ## Removed +# 51.02-r1 + +## Fixes +- `deathcause`: fix error when retrieving the name of a historical figure + # 50.15-r2 ## New Tools From 2c78e441ab0e168ad7fade5d7d1ca9b9ab0446df Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 24 Jan 2025 16:36:24 -0800 Subject: [PATCH 356/811] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 92787ecf93..723599205f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,7 +34,7 @@ Template for new versions: ## Misc Improvements - `assign-preferences`: new ``--show`` option to display the preferences of the selected unit -- `pref-assign`: new ``show`` command to display the preferences of the selected unit +- `pref-adjust`: new ``show`` command to display the preferences of the selected unit ## Removed From ca33684d7dfd82c4f1746609db0cc8dc603c28ad Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Jan 2025 17:26:34 -0800 Subject: [PATCH 357/811] use the new setAutomaticProfessions endpoint --- gui/manipulator.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/manipulator.lua b/gui/manipulator.lua index b3d2edcba2..73cc885eda 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -653,7 +653,8 @@ function Spreadsheet:init() data_fn=curry(toggle_sorted_vec_data, wd.assigned_units), toggle_fn=function(unit_id, prev_val) toggle_sorted_vec(wd.assigned_units, unit_id, prev_val) - -- TODO: poke DF to actually apply the work details to units + local unit = df.unit.find(unit_id) + if unit then dfhack.units.setAutomaticProfessions(unit) end end, } } From 7546d3c3bdb3780963948b319d5c27bd2347d8e3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 24 Jan 2025 21:41:51 -0800 Subject: [PATCH 358/811] hide popups for adventure mode; add reset command --- changelog.txt | 2 ++ docs/hide-tutorials.rst | 14 ++++++++++---- hide-tutorials.lua | 37 ++++++++++++++++++++++++++++--------- 3 files changed, 40 insertions(+), 13 deletions(-) diff --git a/changelog.txt b/changelog.txt index 723599205f..d7218b53d4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,8 @@ Template for new versions: ## Misc Improvements - `assign-preferences`: new ``--show`` option to display the preferences of the selected unit - `pref-adjust`: new ``show`` command to display the preferences of the selected unit +- `hide-tutorials`: if enabled, also hide tutorial popups for adventure mode +- `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game ## Removed diff --git a/docs/hide-tutorials.rst b/docs/hide-tutorials.rst index 417d5e278c..94b2a27a28 100644 --- a/docs/hide-tutorials.rst +++ b/docs/hide-tutorials.rst @@ -3,12 +3,12 @@ hide-tutorials .. dfhack-tool:: :summary: Hide new fort tutorial popups. - :tags: fort interface + :tags: adventure fort interface If you've played the game before and don't need to see the tutorial popups that show up on every new fort, ``hide-tutorials`` can hide them for you. You can enable this tool as a system service in the "Services" tab of -`gui/control-panel` so it takes effect for all new or loaded forts. +`gui/control-panel` so it takes effect for all forts and adventures. Specifically, this tool hides: @@ -16,6 +16,8 @@ Specifically, this tool hides: - The "Do you want to start a tutorial embark" popup - Popups displayed the first time you open the labor, burrows, justice, and other similar screens in a new fort +- Popups displayed when you perform certain actions for the first time in an + adventure Note that only unsolicited tutorial popups are hidden. If you directly request a tutorial page from the help, then it will still function normally. @@ -27,6 +29,10 @@ Usage enable hide-tutorials hide-tutorials + hide-tutorials reset -If you haven't enabled the tool, but you run the command while a fort is -loaded, all future popups for the loaded fort will be hidden. +If you haven't enabled the tool, but you run the command while a fort or +adventure is loaded, all future popups for the loaded game will be hidden. + +If you run the command with the ``reset`` option, all popups will be re-enabled +as if they had never been seen or dismissed. diff --git a/hide-tutorials.lua b/hide-tutorials.lua index 2ca950e3bf..e6124de0de 100644 --- a/hide-tutorials.lua +++ b/hide-tutorials.lua @@ -12,10 +12,6 @@ function isEnabled() return enabled end -local function is_fort_map_loaded() - return df.global.gamemode == df.game_mode.DWARF and dfhack.isMapLoaded() -end - local help = df.global.game.main_interface.help local function close_help() @@ -43,15 +39,36 @@ function skip_tutorial_prompt() end end +local function get_prefix() + if dfhack.world.isFortressMode() then + return 'POPUP_' + elseif dfhack.world.isAdventureMode() then + return 'ADVENTURE_POPUP_' + end +end + local function hide_all_popups() + local prefix = get_prefix() + if not prefix then return end for i,name in ipairs(df.help_context_type) do - if not name:startswith('POPUP_') then goto continue end + if not name:startswith(prefix) then goto continue end utils.insert_sorted(df.global.plotinfo.tutorial_seen, i) utils.insert_sorted(df.global.plotinfo.tutorial_hide, i) ::continue:: end end +local function show_all_popups() + local prefix = get_prefix() + if not prefix then return end + for i,name in ipairs(df.help_context_type) do + if not name:startswith(prefix) then goto continue end + utils.erase_sorted(df.global.plotinfo.tutorial_seen, i) + utils.erase_sorted(df.global.plotinfo.tutorial_hide, i) + ::continue:: + end +end + dfhack.onStateChange[GLOBAL_KEY] = function(sc) if not enabled then return end @@ -65,7 +82,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) dfhack.timeout(100, 'frames', skip_tutorial_prompt) dfhack.timeout(1000, 'frames', skip_tutorial_prompt) end - elseif sc == SC_MAP_LOADED and df.global.gamemode == df.game_mode.DWARF then + elseif sc == SC_MAP_LOADED then hide_all_popups() end end @@ -81,13 +98,15 @@ end if args[1] == "enable" then enabled = true - if is_fort_map_loaded() then + if dfhack.isMapLoaded() then hide_all_popups() end elseif args[1] == "disable" then enabled = false -elseif is_fort_map_loaded() then +elseif args[1] == "reset" then + show_all_popups() +elseif dfhack.isMapLoaded() then hide_all_popups() else - qerror('hide-tutorials needs a loaded fortress map to work') + qerror('hide-tutorials needs a loaded fortress or adventure map to work') end From e07775adb2a58ca23b37fc3841b9e086c3cf09bb Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 25 Jan 2025 10:29:06 -0800 Subject: [PATCH 359/811] remove duplicate changelog line --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 38eda54312..fe674b7019 100644 --- a/changelog.txt +++ b/changelog.txt @@ -51,7 +51,6 @@ Template for new versions: ## New Tools - `fix/stuck-squad`: allow squads and messengers returning from missions to rescue squads that have gotten stuck on the world map -- `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (e.g. units, governments, fortresses, or the world) - `gui/rename`: (reinstated) give new in-game language-based names to anything that can be named (units, governments, fortresses, the world, etc.) ## New Features From 1757cf407a967b4c5a9d35fa061aa854e5c6ff78 Mon Sep 17 00:00:00 2001 From: Alex Noir Date: Sun, 26 Jan 2025 23:34:38 +0300 Subject: [PATCH 360/811] Fix advtools convo using the wrong talk_choice_type for creating new convo option, resulting in a nil value being passed --- internal/advtools/convo.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/advtools/convo.lua b/internal/advtools/convo.lua index d8b9b06a37..2b66a4ed66 100644 --- a/internal/advtools/convo.lua +++ b/internal/advtools/convo.lua @@ -81,7 +81,7 @@ local function addWhereaboutsChoice(race, name, target_id, heard_of) if heard_of then title = title .. " (Heard of)" end - local choice = new_choice(df.talk_choice_type.AskWhereabouts, title, dfhack.translation.translateName(name):split()) + local choice = new_choice(df.talk_choice_type.AskForDirectionsToHF, title, dfhack.translation.translateName(name):split()) -- insert before the last choice, which is usually "back" adventure.conversation.conv_choice_info:insert(#adventure.conversation.conv_choice_info-1, choice) choice.choice.invocation_target_hfid = target_id From 45c1997ec1077abd50ddefed6003e22df22777c0 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 26 Jan 2025 14:42:16 -0600 Subject: [PATCH 361/811] Update hello-world.lua --- devel/hello-world.lua | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/devel/hello-world.lua b/devel/hello-world.lua index 5e8b22ebb6..12b9eab7c3 100644 --- a/devel/hello-world.lua +++ b/devel/hello-world.lua @@ -17,7 +17,7 @@ HelloWorldWindow.ATTRS{ autoarrange_subviews=true, autoarrange_gap=2, resizable=true, - resize_min={w=25, h=20}, + resize_min={w=25, h=25}, } function HelloWorldWindow:init() @@ -32,7 +32,7 @@ function HelloWorldWindow:init() self:addviews{ widgets.Label{text={{text='Hello, world!', pen=COLOR_LIGHTGREEN}}}, widgets.HotkeyLabel{ - frame={l=0, t=0}, + frame={l=0}, label='Click me', key='CUSTOM_CTRL_A', on_activate=self:callback('toggleHighlight'), @@ -43,23 +43,25 @@ function HelloWorldWindow:init() frame_style=gui.INTERIOR_FRAME, }, widgets.Divider{ - frame={l=0,t=3} + frame={h=1}, + frame_style_l=false, + frame_style_r=false, }, widgets.CycleHotkeyLabel{ view_id='level', - frame={l=0, t=3, w=16}, + frame={l=0, w=20}, label='Level:', - label_below=true, + label_below=false, key_back='CUSTOM_SHIFT_C', key='CUSTOM_SHIFT_V', options=LEVEL_OPTIONS, initial_option=LEVEL_OPTIONS[1].value, on_change=function(val) - self.subviews.level:setOption(val) + self.callback{Slider.on_change(val)} end, }, widgets.Slider{ - frame={l=1, t=3}, + frame={l=1}, num_stops=#LEVEL_OPTIONS, get_idx_fn=function() return self.subviews.level:getOptionValue() From 791096c5425b03ce293991f05fe90db6159213b2 Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 26 Jan 2025 20:21:51 -0800 Subject: [PATCH 362/811] Update changelog --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index fe674b7019..f28a5bbcbb 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,12 +27,12 @@ Template for new versions: # Future ## New Tools - - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk ## New Features ## Fixes +- `advtools`: fix dfhack-added conversation options not appearing in the ask whereabouts conversation tree ## Misc Improvements - `assign-preferences`: new ``--show`` option to display the preferences of the selected unit From 1b4199c52f190054883a3ffc57dc2f23674577f2 Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sun, 26 Jan 2025 23:46:19 -0500 Subject: [PATCH 363/811] Fix typo causing error when changing language --- changelog.txt | 1 + gui/rename.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index f28a5bbcbb..6c8f0dd35e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes - `advtools`: fix dfhack-added conversation options not appearing in the ask whereabouts conversation tree +- `gui/rename`: fix error when changing the language of a unit's name ## Misc Improvements - `assign-preferences`: new ``--show`` option to display the preferences of the selected unit diff --git a/gui/rename.lua b/gui/rename.lua index 40ce109018..437e6ffc7f 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -704,7 +704,7 @@ function Rename:set_language(val, prev_val) sync_name() else sync_name.language = val - sync_name.first_name = self.target.first_name + sync_name.first_name = self.target.name.first_name end end end From 5bab7332fb4e94037b722bdb8a1b212e7e59c82b Mon Sep 17 00:00:00 2001 From: Crystalwarrior Date: Tue, 28 Jan 2025 10:28:12 +0300 Subject: [PATCH 364/811] Refactor unretire-anyone code and make it a module so you can access its naming scheme function (#1231) * Refactor unretire-anyone code and make it a module so you can access its naming scheme function * Fix dead adventurers not being pickable for unretire even with --d flag * Added number of entries to be shown in the ui --- unretire-anyone.lua | 103 ++++++++++++++++++++++++++------------------ 1 file changed, 61 insertions(+), 42 deletions(-) diff --git a/unretire-anyone.lua b/unretire-anyone.lua index be2a50c1a0..dbf56ddc7e 100644 --- a/unretire-anyone.lua +++ b/unretire-anyone.lua @@ -1,3 +1,5 @@ +--@module=true + local options = {} local argparse = require('argparse') @@ -5,15 +7,10 @@ local commands = argparse.processArgsGetopt({ ... }, { { 'd', 'dead', handler = function() options.dead = true end } }) -local dialogs = require 'gui.dialogs' - -local viewscreen = dfhack.gui.getDFViewscreen(true) -if viewscreen._type ~= df.viewscreen_setupadventurest then - qerror("This script can only be used during adventure mode setup!") -end +local dialogs = require('gui.dialogs') --luacheck: in=df.viewscreen_setupadventurest,df.nemesis_record -function addNemesisToUnretireList(advSetUpScreen, nemesis, index) +local function addNemesisToUnretireList(advSetUpScreen, nemesis, index) local unretireOption = false for i = #advSetUpScreen.valid_race - 1, 0, -1 do if advSetUpScreen.valid_race[i] == -2 then -- this is the "Specific Person" option on the menu @@ -39,59 +36,81 @@ function addNemesisToUnretireList(advSetUpScreen, nemesis, index) advSetUpScreen.nemesis_index:insert('#', index) end +function getHistfigShortSummary(histFig) + local race = df.creature_raw.find(histFig.race) + local name = 'Unknown Creature' + local sym = nil + if race then + local creature = race.caste[histFig.caste] + name = creature.caste_name[0] + sym = df.pronoun_type.attrs[creature.sex].symbol + end + if histFig.info and histFig.info.curse then + local curse = histFig.info.curse + if curse.name ~= '' then + name = name .. ' ' .. curse.name + end + if curse.undead_name ~= '' then + name = curse.undead_name .. " - reanimated " .. name + end + end + if histFig.flags.ghost then + name = name .. " ghost" + end + if sym then + name = name .. ' (' .. sym .. ')' + end + name = name .. + '\n' .. dfhack.units.getReadableName(histFig) + if histFig.name.has_name then + name = name .. + '\n"' .. dfhack.translation.translateName(histFig.name, true) .. '"' + else + name = name .. + '\nUnnamed' + end + return name +end + --luacheck: in=table -function showNemesisPrompt(advSetUpScreen) +local function showNemesisPrompt(advSetUpScreen) local choices = {} for i, nemesis in ipairs(df.global.world.nemesis.all) do - if nemesis.figure and not nemesis.flags.ADVENTURER then -- these are already available for unretiring + if nemesis.figure then local histFig = nemesis.figure - local histFlags = histFig.flags - if (histFig.died_year == -1 or histFlags.ghost or options.dead) and - not histFlags.deity and - not histFlags.force + if (histFig.died_year == -1 or histFig.flags.ghost or options.dead) and + not histFig.flags.deity and + not histFig.flags.force then - local creature = dfhack.units.getCasteRaw(histFig.race, histFig.caste) - local name = creature.caste_name[0] - if histFig.info and histFig.info.curse then - local curse = histFig.info.curse - if curse.name ~= '' then - name = name .. ' ' .. curse.name - end - if curse.undead_name ~= '' then - name = curse.undead_name .. " - reanimated " .. name - end - end - if histFlags.ghost then - name = name .. " ghost" - end - local sym = df.pronoun_type.attrs[creature.sex].symbol - if sym then - name = name .. ' (' .. sym .. ')' - end - if histFig.name.has_name then - name = name .. - '\n' .. dfhack.translation.translateName(histFig.name) .. - '\n"' .. dfhack.translation.translateName(histFig.name, true) .. '"' - else - name = name .. - '\nUnnamed' + if histFig.died_year == -1 and nemesis.flags.ADVENTURER then + -- already available for unretiring + goto continue end + local name = getHistfigShortSummary(histFig) table.insert(choices, { text = name, nemesis = nemesis, search_key = name:lower(), idx = i }) end end + ::continue:: end dialogs.ListBox{ frame_title = 'unretire-anyone', - text = 'Select someone to add to the "Specific Person" list:', + text = 'Select someone to add to the "Specific Person" list (' .. #choices .. ' entries)', text_pen = COLOR_WHITE, choices = choices, - on_select = function(id, choice) - addNemesisToUnretireList(advSetUpScreen, choice.nemesis, choice.idx) - end, + on_select = function(id, choice) addNemesisToUnretireList(advSetUpScreen, choice.nemesis, choice.idx) end, with_filter = true, row_height = 3, }:show() end +if dfhack_flags.module then + return +end + +local viewscreen = dfhack.gui.getDFViewscreen(true) +if viewscreen._type ~= df.viewscreen_setupadventurest then + qerror("This script can only be used during adventure mode setup!") +end + showNemesisPrompt(viewscreen) From d68e0cebc9da3fad475d40baf4016c2c5c8323fb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 27 Jan 2025 23:31:54 -0800 Subject: [PATCH 365/811] improve formatting of list entries --- unretire-anyone.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/unretire-anyone.lua b/unretire-anyone.lua index dbf56ddc7e..8ca19ff886 100644 --- a/unretire-anyone.lua +++ b/unretire-anyone.lua @@ -61,10 +61,10 @@ function getHistfigShortSummary(histFig) name = name .. ' (' .. sym .. ')' end name = name .. - '\n' .. dfhack.units.getReadableName(histFig) + '\n ' .. dfhack.units.getReadableName(histFig) if histFig.name.has_name then name = name .. - '\n"' .. dfhack.translation.translateName(histFig.name, true) .. '"' + '\n "' .. dfhack.translation.translateName(histFig.name, true) .. '"' else name = name .. '\nUnnamed' @@ -95,7 +95,7 @@ local function showNemesisPrompt(advSetUpScreen) dialogs.ListBox{ frame_title = 'unretire-anyone', - text = 'Select someone to add to the "Specific Person" list (' .. #choices .. ' entries)', + text = 'Select someone to add to the "Specific Person" list:', text_pen = COLOR_WHITE, choices = choices, on_select = function(id, choice) addNemesisToUnretireList(advSetUpScreen, choice.nemesis, choice.idx) end, From 5cbfb58f616a6603c146606585e7c277a6dc52be Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 30 Jan 2025 02:42:43 -0800 Subject: [PATCH 366/811] add pull request template --- .github/pull_request_template.md | 1 + 1 file changed, 1 insertion(+) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000000..e682b17afc --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1 @@ +If this PR makes an externally-visible change in behavior, please add an appropriate line to `changelog.txt`. From 962b4b31f5e0af35633064b83eb6c5c3a2bc4906 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 30 Jan 2025 14:13:26 -0800 Subject: [PATCH 367/811] Update position.lua * Make it a module * Use argparse * Adventure mode cursor support * Give mod16 offset for cursor * Improve code --- position.lua | 172 +++++++++++++++++++++++++++++++++------------------ 1 file changed, 111 insertions(+), 61 deletions(-) diff --git a/position.lua b/position.lua index de6248f62f..66404dd500 100644 --- a/position.lua +++ b/position.lua @@ -1,20 +1,36 @@ +-- Detailed info about time and place. Can also copy keyboard cursor position. +--@ module = true -local cursor = df.global.cursor -local args = {...} -if #args > 0 then --Copy keyboard cursor to clipboard - if #args > 1 then - qerror('Too many arguments!') - elseif args[1] ~= '-c' and args[1] ~= '--copy' then - qerror('Invalid argument "'..args[1]..'"!') - elseif cursor.x < 0 then - qerror('No keyboard cursor!') +local argparse = require('argparse') + +local function parse_args(args) + local opts = {} + local positionals = argparse.processArgsGetopt(args, + { + {'c', 'copy', handler=function() opts.copy = true end}, + }) + + if #positionals > 0 then + qerror('Too many positionals!') end - dfhack.internal.setClipboardTextCp437(('%d,%d,%d'):format(cursor.x, cursor.y, cursor.z)) - return + return opts +end + +function get_active_cursor() --Return active fort/adv cursor or nil + if dfhack.world.isAdventureMode() then + local look = df.global.game.main_interface.adventure.look + if look.open and look.cursor:isValid() then + return look.cursor --Note: This is a df.coord + end + elseif df.global.cursor.x >= 0 then + return df.global.cursor + end + return nil --Not active end -local months = { +local months = +{ 'Granite, in early Spring.', 'Slate, in mid Spring.', 'Felsite, in late Spring.', @@ -29,68 +45,102 @@ local months = { 'Obsidian, in late Winter.', } ---Fortress mode counts 1200 ticks per day and 403200 per year ---Adventurer mode counts 86400 ticks to a day and 29030400 ticks per year ---Twelve months per year, 28 days to every month, 336 days per year +function print_time_info() + --Fortress mode counts 1200 ticks per day and 403200 per year + --Adventurer mode counts 86400 ticks to a day and 29030400 ticks per year + --Twelve months per year, 28 days to every month, 336 days per year + local julian_day = df.global.cur_year_tick // 1200 + 1 + local month = julian_day // 28 + 1 --days and months are 1-indexed + local day = julian_day % 28 -local julian_day = df.global.cur_year_tick // 1200 + 1 -local month = julian_day // 28 + 1 --days and months are 1-indexed -local day = julian_day % 28 + local time_of_day = df.global.cur_year_tick_advmode // 336 + local second = time_of_day % 60 + local minute = time_of_day // 60 % 60 + local hour = time_of_day // 3600 % 24 -local time_of_day = df.global.cur_year_tick_advmode // 336 -local second = time_of_day % 60 -local minute = time_of_day // 60 % 60 -local hour = time_of_day // 3600 % 24 + print('Time:') + print((' The time is %02d:%02d:%02d'):format(hour, minute, second)) + print((' The date is %03d-%02d-%02d'):format(df.global.cur_year, month, day)) + print(' It is the month of '..months[month]) -print('Time:') -print((' The time is %02d:%02d:%02d'):format(hour, minute, second)) -print((' The date is %03d-%02d-%02d'):format(df.global.cur_year, month, day)) -print(' It is the month of '..months[month]) + local eras = df.global.world.history.eras + if #eras > 0 then + print(' It is the '..eras[#eras-1].title.name..'.') + end +end -local eras = df.global.world.history.eras -if #eras > 0 then - print(' It is the '..eras[#eras-1].title.name..'.') +function get_adv_region_pos() --Regional coords + if not dfhack.world.getAdventurer() then --Army exists when unit doesn't + local army = df.army.find(df.global.adventure.player_army_id) + if army then + return army.pos.x//48, army.pos.y//48 + end + end + local wd = df.global.world.world_data + return wd.midmap_data.adv_region_x, wd.midmap_data.adv_region_y end -print('Place:') -print(' The z-level is z='..df.global.window_z) +local function print_world_info() + local wd = df.global.world.world_data + local site = dfhack.world.getCurrentSite() + if site then + print((' The current site is at x=%d, y=%d on the %dx%d world map.'): + format(site.pos.x, site.pos.y, wd.world_width, wd.world_height)) + end -if cursor.x < 0 then - print(' The keyboard cursor is inactive.') -else - print(' The keyboard cursor is at x='..cursor.x..', y='..cursor.y) + if dfhack.world.isAdventureMode() then + local x, y = get_adv_region_pos() + print((' The adventurer is at x=%d, y=%d on the %dx%d world map.'): + format(x, y, wd.world_width, wd.world_height)) + end end -local x, y = dfhack.screen.getWindowSize() -print(' The window is '..x..' tiles wide and '..y..' tiles high.') +function print_place_info(cursor) + print('Place:') + print(' The z-level is z='..df.global.window_z) -x, y = dfhack.screen.getMousePos() -if x then - print(' The mouse is at x='..x..', y='..y..' within the window.') - local pos = dfhack.gui.getMousePos() - if pos then - print(' The mouse is over map tile x='..pos.x..', y='..pos.y) + if cursor then + local x, y = cursor.x, cursor.y + print((' The keyboard cursor is at x=%d, y=%d (%d+%d, %d+%d)'): + format(x, y, x//16*16, x%16, y//16*16, y%16)) + else + print(' The keyboard cursor is inactive.') end -else - print(' The mouse is not in the DF window.') -end -local wd = df.global.world.world_data -local site = dfhack.world.getCurrentSite() -if site then - print((' The current site is at x=%d, y=%d on the %dx%d world map.'): - format(site.pos.x, site.pos.y, wd.world_width, wd.world_height)) -elseif dfhack.world.isAdventureMode() then - x, y = -1, -1 - for _,army in ipairs(df.global.world.armies.all) do - if army.flags.player then - x, y = army.pos.x // 48, army.pos.y // 48 - break + local x, y = dfhack.screen.getWindowSize() + print(' The window is '..x..' tiles wide and '..y..' tiles high.') + + x, y = dfhack.screen.getMousePos() + if x then + print(' The mouse is at x='..x..', y='..y..' within the window.') + local pos = dfhack.gui.getMousePos() + if pos then + print(' The mouse is over map tile x='..pos.x..', y='..pos.y) end + else + print(' The mouse is not in the DF window.') end - if x < 0 then - x, y = wd.midmap_data.adv_region_x, wd.midmap_data.adv_region_y + + print_world_info() +end + +if dfhack_flags.module then + return +end + +function main(opts) + local cursor = get_active_cursor() + + if opts.copy then --Copy keyboard cursor to clipboard + if not cursor then + qerror('No keyboard cursor!') + end + dfhack.internal.setClipboardTextCp437(('%d,%d,%d'):format(cursor.x, cursor.y, cursor.z)) + return --Don't print anything end - print((' The adventurer is at x=%d, y=%d on the %dx%d world map.'): - format(x, y, wd.world_width, wd.world_height)) + + print_time_info() + print_place_info(cursor) end + +main(parse_args({...})) From 02d87db0f9033d728779febfb4d499f498d638d9 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 30 Jan 2025 14:19:04 -0800 Subject: [PATCH 368/811] Update position.lua --- position.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/position.lua b/position.lua index 66404dd500..49cf1f3526 100644 --- a/position.lua +++ b/position.lua @@ -1,4 +1,4 @@ --- Detailed info about time and place. Can also copy keyboard cursor position. +-- Report cursor and mouse position, along with other info. --@ module = true local argparse = require('argparse') From c5658cdfe09e29c236976245fc5081e6c3b9c943 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 30 Jan 2025 14:21:26 -0800 Subject: [PATCH 369/811] Update position.rst --- docs/position.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/position.rst b/docs/position.rst index 9d06d39618..56be5993c7 100644 --- a/docs/position.rst +++ b/docs/position.rst @@ -8,7 +8,7 @@ position This tool reports the current date, clock time, month, season, and historical era. It also reports the keyboard cursor position (or just the z-level if no active cursor), window size, and mouse location on the screen. If a site is -loaded, it prints the world coordinates of the site. If not, it prints the world +loaded, it prints the world coordinates of the site. It also prints the world coordinates of the adventurer (if applicable). Can also be used to copy the current keyboard cursor position for later use. From 11f749b42cf868c405e0a8c0c78a806d95b3c54b Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 30 Jan 2025 14:24:16 -0800 Subject: [PATCH 370/811] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 6c8f0dd35e..ea40f1ddfe 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: ## Fixes - `advtools`: fix dfhack-added conversation options not appearing in the ask whereabouts conversation tree - `gui/rename`: fix error when changing the language of a unit's name +- `position`: support for adv mode look cursor ## Misc Improvements - `assign-preferences`: new ``--show`` option to display the preferences of the selected unit From 7e53def023a67c14673bd6a89a3fa397f59310f0 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 1 Feb 2025 11:16:27 +0100 Subject: [PATCH 371/811] render mouse tooltips over unit banners Also, don't import ResizingPanel separately --- gui/tooltips.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index 31eee1510f..746bf881d9 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -7,7 +7,6 @@ local RELOAD = false -- set to true when actively working on this script local gui = require('gui') local widgets = require('gui.widgets') local overlay = require('plugins.overlay') -local ResizingPanel = require('gui.widgets.containers.resizing_panel') -------------------------------------------------------------------------------- @@ -233,7 +232,7 @@ end -- MouseTooltip is an almost copy&paste of the DimensionsTooltip -- if RELOAD then MouseTooltip = nil end -MouseTooltip = defclass(MouseTooltip, ResizingPanel) +MouseTooltip = defclass(MouseTooltip, widgets.ResizingPanel) MouseTooltip.ATTRS{ frame_style=gui.FRAME_THIN, @@ -330,8 +329,11 @@ local function GetScreenCoordinates(map_coord) end function TooltipsOverlay:render(dc) + self:render_unit_banners(dc) TooltipsOverlay.super.render(self, dc) +end +function TooltipsOverlay:render_unit_banners(dc) if not config.follow_units then return end if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then From 33635a6efbefd1eeebab677f18937bd5df89b764 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 1 Feb 2025 02:51:21 -0800 Subject: [PATCH 372/811] remove craft-age-wear tweak from the control panel for Windows users. until https://github.com/DFHack/dfhack/issues/4292 is fixed --- changelog.txt | 1 + internal/control-panel/registry.lua | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 6c8f0dd35e..9e63092f48 100644 --- a/changelog.txt +++ b/changelog.txt @@ -42,6 +42,7 @@ Template for new versions: - `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game ## Removed +- `gui/control-panel`: removed ``craft-age-wear`` tweak for Windows users; the tweak doesn't currently load on Windows # 51.02-r1 diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 6ffd0e3a7b..6b9213c9b0 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -68,8 +68,9 @@ COMMANDS_BY_IDX = { -- bugfix tools {command='adamantine-cloth-wear', help_command='tweak', group='bugfix', mode='tweak', default=true, desc='Prevents adamantine clothing from wearing out while being worn.'}, - {command='craft-age-wear', help_command='tweak', group='bugfix', mode='tweak', default=true, - desc='Allows items crafted from organic materials to wear out over time.'}, + -- re-inserted below for non-Windows users (where the tweak doesn't work) + -- {command='craft-age-wear', help_command='tweak', group='bugfix', mode='tweak', default=true, + -- desc='Allows items crafted from organic materials to wear out over time.'}, {command='fix/blood-del', group='bugfix', mode='run', default=true}, {command='fix/dead-units', group='bugfix', mode='repeat', default=true, desc='Fix units still being assigned to burrows after death.', @@ -139,6 +140,15 @@ COMMANDS_BY_IDX = { {command='work-now', group='gameplay', mode='enable'}, } +-- temporary workaround for Windows users until the tweak works +if dfhack.getOSType() ~= 'windows' then + local idx = utils.linear_index(COMMANDS_BY_IDX, 'adamantine-cloth-wear', 'command') or 1 + table.insert(COMMANDS_BY_IDX, idx + 1, { + command='craft-age-wear', help_command='tweak', group='bugfix', mode='tweak', default=true, + desc='Allows items crafted from organic materials to wear out over time.', + }) +end + COMMANDS_BY_NAME = {} for _,data in ipairs(COMMANDS_BY_IDX) do COMMANDS_BY_NAME[data.command] = data From ec27b390e550f1ac89b0060318115e9aa52d554a Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 1 Feb 2025 12:22:59 +0100 Subject: [PATCH 373/811] use list instead of labels in config UI --- gui/tooltips.lua | 114 +++++++++++++++++++++++------------------------ 1 file changed, 55 insertions(+), 59 deletions(-) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index 746bf881d9..e08b97e608 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -66,6 +66,10 @@ if RELOAD then config = nil end config = config or { follow_units = true, follow_mouse = false, + show_unit_jobs = true, + job_shortenings = { + ["Store item in stockpile"] = "Store item", + }, show_happiness = true, happiness_levels = { -- keep in mind, the text will look differently with game's font @@ -79,10 +83,6 @@ config = config or { {text = ":D", pen = COLOR_LIGHTGREEN, visible = true, name = "Happy"}, {text = "=D", pen = COLOR_LIGHTCYAN, visible = true, name = "Ecstatic"}, }, - show_unit_jobs = true, - job_shortenings = { - ["Store item in stockpile"] = "Store item", - } } -------------------------------------------------------------------------------- @@ -123,69 +123,65 @@ TooltipControlWindow.ATTRS { }, } --- right pad string `s` to `n` symbols with spaces -local function rpad(s, n) - local formatStr = "%-" .. n .. "s" -- `"%-10s"` - return string.format(formatStr, s) +local function make_enabled_text(indent, text, cfg, key) + local function get_enabled_button_token(enabled_tile, disabled_tile, cfg, key) + return { + tile=function() return cfg[key] and enabled_tile or disabled_tile end, + } + end + + local tokens = { + string.format("%" .. indent .. "s", ''), + get_enabled_button_token(ENABLED_PEN_LEFT, DISABLED_PEN_LEFT, cfg, key), + get_enabled_button_token(ENABLED_PEN_CENTER, DISABLED_PEN_CENTER, cfg, key), + get_enabled_button_token(ENABLED_PEN_RIGHT, DISABLED_PEN_RIGHT, cfg, key), + ' ', + } + if type(text) == 'string' then + tokens[#tokens+1] = text + else -- must be a table + -- append it + for _, v in ipairs(text) do + tokens[#tokens+1] = v + end + end + + return tokens +end + +local function make_choice(indent, text, cfg, key) + return { + text=make_enabled_text(indent, text, cfg, key), + data={cfg=cfg, key=key}, + } end function TooltipControlWindow:init() - local w = self.frame.w - 2 - 3 -- 2 is border, 3 is active indicator width - local keyW = 7 -- Length of "Alt+u: " + local choices = {} + table.insert(choices, make_choice(0, "unit banners", config, "follow_units")) + table.insert(choices, make_choice(0, "mouse tooltips", config, "follow_mouse")) + table.insert(choices, make_choice(0, "include jobs", config, "show_unit_jobs")) + table.insert(choices, make_choice(0, "include stress levels", config, "show_happiness")) + for i = 0, #config.happiness_levels do + local cfg = config.happiness_levels[i] + table.insert(choices, make_choice(3, {{text=cfg.text, pen=cfg.pen}, ' ', cfg.name}, cfg, "visible")) + end self:addviews{ - ToggleLabel{ - view_id = 'btn_follow_units', - frame={t=0, h=1}, - label=rpad("Unit banners", w - keyW), - key='CUSTOM_ALT_U', - initial_option=config.follow_units, - on_change=function(new) config.follow_units = new end, - }, - ToggleLabel{ - view_id = 'btn_follow_mouse', - frame={t=1, h=1}, - label=rpad("Mouse tooltip", w - keyW), - key='CUSTOM_ALT_M', - initial_option=config.follow_mouse, - on_change=function(new) config.follow_mouse = new end, - }, - ToggleLabel{ - frame={t=2, h=1}, - label=rpad("Show jobs", w), - initial_option=config.show_unit_jobs, - on_change=function(new) config.show_unit_jobs = new end, - }, - ToggleLabel{ - frame={t=3, h=1}, - label=rpad("Show stress levels", w), - initial_option=config.show_happiness, - on_change=function(new) config.show_happiness = new end, + widgets.List{ + frame={t=0}, + view_id='list', + on_submit=self:callback('on_submit'), + row_height=1, + choices = choices, }, } +end - local happinessLabels = {} - - -- align the emoticons - local maxNameLength = 1 - for _, v in pairs(config.happiness_levels) do - local l = #v.name - if l > maxNameLength then - maxNameLength = l - end - end - - local indent = 3 - for lvl, cfg in pairs(config.happiness_levels) do - happinessLabels[#happinessLabels + 1] = ToggleLabel{ - frame={t=4+lvl, h=1, l=indent}, - initial_option=cfg.visible, - text_pen = cfg.pen, - label = rpad(rpad(cfg.name, maxNameLength) .. " " .. cfg.text, w - indent), - on_change = function(new) cfg.visible = new end - } - end - self:addviews(happinessLabels) +function TooltipControlWindow:on_submit(index, choice) + local cfg = choice.data.cfg + local key = choice.data.key + cfg[key] = not cfg[key] end local function GetUnitHappiness(unit) From 5ec1e544798889416e139ce365f99b2d92e81e7b Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 1 Feb 2025 21:31:44 +0100 Subject: [PATCH 374/811] persist config (globally) --- gui/tooltips.lua | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/gui/tooltips.lua b/gui/tooltips.lua index e08b97e608..abd9acf8d6 100644 --- a/gui/tooltips.lua +++ b/gui/tooltips.lua @@ -5,6 +5,7 @@ local RELOAD = false -- set to true when actively working on this script local gui = require('gui') +local utils = require('utils') local widgets = require('gui.widgets') local overlay = require('plugins.overlay') @@ -85,6 +86,41 @@ config = config or { }, } +-------------------------------------------------------------------------------- +-- config persistence +local CONFIG_FILE_PATH = 'dfhack-config/tooltips.json' + +local function load_config() + local json = require('json') + + local f = json.open(CONFIG_FILE_PATH) + if f.exists then + -- remove unknown or out of date entries from the loaded config + -- shallow search should be enough + for k in pairs(f.data) do + if config[k] == nil then + f.data[k] = nil + end + end + + -- convert string keys into numbers - workaround json (encoder) limitations + ensure_key(f.data, "happiness_levels") + local t = f.data.happiness_levels + for k, v in pairs(t) do + t[tonumber(k)] = v + t[k] = nil + end + + utils.assign(config, f.data) + end + + f.data = config -- link the config info with the file + f:write() -- possibly update the stored config + return f +end + +local config_file = load_config() + -------------------------------------------------------------------------------- local TITLE = "Tooltips" @@ -182,6 +218,8 @@ function TooltipControlWindow:on_submit(index, choice) local cfg = choice.data.cfg local key = choice.data.key cfg[key] = not cfg[key] + + config_file:write() end local function GetUnitHappiness(unit) From 6936ce8de8db5b00c490172159a1ab7ffbda28ca Mon Sep 17 00:00:00 2001 From: Crystalwarrior Date: Sat, 1 Feb 2025 23:57:25 +0300 Subject: [PATCH 375/811] Add new overlay advtools fastcombat - skip combat anims and announcements (#1385) Add a new fastcombat advtools overlay, allowing you to instantly process many combat actions to combat the tedium of combat. Also works for movement in general! --------- Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com> Co-authored-by: Myk --- advtools.lua | 2 + changelog.txt | 1 + docs/advtools.rst | 13 +++++ internal/advtools/fastcombat.lua | 89 ++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+) create mode 100644 internal/advtools/fastcombat.lua diff --git a/advtools.lua b/advtools.lua index 6b2b0d09af..9f4d2ec3b3 100644 --- a/advtools.lua +++ b/advtools.lua @@ -1,10 +1,12 @@ --@ module=true local convo = reqscript('internal/advtools/convo') +local fastcombat = reqscript('internal/advtools/fastcombat') local party = reqscript('internal/advtools/party') OVERLAY_WIDGETS = { conversation=convo.AdvRumorsOverlay, + fastcombat=fastcombat.AdvCombatOverlay, } if dfhack_flags.module then diff --git a/changelog.txt b/changelog.txt index 9e63092f48..1de9ccde64 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,7 @@ Template for new versions: - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk ## New Features +- `advtools`: new overlay ``advtools.fastcombat``; allows you to skip combat animations and the announcement "More" button ## Fixes - `advtools`: fix dfhack-added conversation options not appearing in the ask whereabouts conversation tree diff --git a/docs/advtools.rst b/docs/advtools.rst index c62cdb1f83..113d6c969a 100644 --- a/docs/advtools.rst +++ b/docs/advtools.rst @@ -35,3 +35,16 @@ enemies will gain the ``slay`` and ``kill`` keywords. It will also add additional conversation options for asking whereabouts of your relationships -- in vanilla, you can only ask whereabouts of historical figures involved in rumors you personally witnessed or heard about. + +``advtools.fastcombat`` +~~~~~~~~~~~~~~~~~~~~~~~ + +When enabled, this overlay will allow you to skip most combat animations, +including the whooshes and projectiles travelling through the screen. It will +also let you skip the announcements window when the "More" button is active, +scrolling you to the very bottom with the first press, and skipping the window +entirely with the second press. This drastically speeds up combat while still +giving you the option not to skip the announcements. Skip keys are left mouse click, +the SELECT button, the movement keys and combat-related keys that don't bring up a +menu (such as bump attack). If clicking to skip past combat, it will only skip the +announcements if you're clicking outside the announcements panel. diff --git a/internal/advtools/fastcombat.lua b/internal/advtools/fastcombat.lua new file mode 100644 index 0000000000..2b9cd184f0 --- /dev/null +++ b/internal/advtools/fastcombat.lua @@ -0,0 +1,89 @@ +--@ module=true +local overlay = require('plugins.overlay') +local gui = require('gui') +local widgets = require('gui.widgets') + +-- Overlay +AdvCombatOverlay = defclass(AdvCombatOverlay, overlay.OverlayWidget) +AdvCombatOverlay.ATTRS{ + desc='Skip combat animations and announcements with a click or key press.', + default_enabled=true, + viewscreens='dungeonmode', + fullscreen=true, + default_pos={x=1, y=7}, + frame={h=15}, +} + +function AdvCombatOverlay:init() + self.skip_combat = false + + self:addviews{ + widgets.Panel{ + frame={w=113}, + view_id='announcement_panel_mask' + } + } +end + +function AdvCombatOverlay:preUpdateLayout(parent_rect) + self.frame.w = parent_rect.width +end + +function AdvCombatOverlay:render(dc) + if df.global.adventure.player_control_state == df.adventurest.T_player_control_state.TAKING_INPUT then + self.skip_combat = false + return + end + if self.skip_combat then + -- Instantly process the projectile travelling + df.global.adventure.projsubloop_visible_projectile = false + -- Skip the combat swing animations + df.global.adventure.game_loop_animation_timer_start = df.global.adventure.game_loop_animation_timer_start + 1000 + end +end + + +local COMBAT_MOVE_KEYS = { + _MOUSE_L=true, + SELECT=true, + A_MOVE_N=true, + A_MOVE_S=true, + A_MOVE_E=true, + A_MOVE_W=true, + A_MOVE_NW=true, + A_MOVE_NE=true, + A_MOVE_SW=true, + A_MOVE_SE=true, + A_MOVE_SAME_SQUARE=true, + A_ATTACK=true, + A_COMBAT_ATTACK=true, +} + +function AdvCombatOverlay:onInput(keys) + for code,_ in pairs(keys) do + if not COMBAT_MOVE_KEYS[code] then goto continue end + if df.global.adventure.player_control_state ~= df.adventurest.T_player_control_state.TAKING_INPUT then + -- Instantly speed up the combat + self.skip_combat = true + elseif df.global.world.status.temp_flag.adv_showing_announcements then + -- Don't let mouse skipping work when you click within the adventure mode announcement panel + if keys._MOUSE_L and self.subviews.announcement_panel_mask:getMousePos() then + return + end + -- Instantly process the projectile travelling + -- (for some reason, projsubloop is still active during "TAKING INPUT" phase) + df.global.adventure.projsubloop_visible_projectile = false + + -- If there is more to be seen in this box... + if df.global.world.status.temp_flag.adv_have_more then + -- Scroll down to the very bottom + df.global.world.status.adv_scroll_position = #df.global.world.status.adv_announcement - 10 + -- Nothing new left to see, get us OUT OF HERE!! + else + -- Allow us to quit out of showing announcements by clicking anywhere OUTSIDE the box + df.global.world.status.temp_flag.adv_showing_announcements = false + end + end + ::continue:: + end +end From 8529442daa157db9f9d35bac9da9140f9779658d Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:03:17 -0600 Subject: [PATCH 376/811] Add liquid_type and flow_size options --- export-map.lua | 35 ++++++++++++++++++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/export-map.lua b/export-map.lua index 16691f8a40..a93c4230b8 100644 --- a/export-map.lua +++ b/export-map.lua @@ -19,6 +19,12 @@ for _, feature in ipairs(df.global.world.features.map_features) do end end +-- right now the only tile_liquids are Water and Magma +local liquid_list = {} +for id, liquid in ipairs(df.tile_liquid) do + liquid_list[id] = liquid +end + -- copied from agitation-rebalance.lua -- check only one tile at the center of the map at ground lvl -- (this ignores different biomes on the edges of the map) @@ -65,6 +71,16 @@ local function classify_tile(options, x, y, z) tile_data[position] = tileflags.subterranean elseif(map_option == "outside") then tile_data[position] = tileflags.outside + elseif(map_option == "liquid") then + if(tileflags.flow_size > 0) then + -- liquid_type is a boolean (true=Magma, false=Water) + -- converting it to a number for easy reference in key table + tile_data[position] = tileflags.liquid_type and 1 or 0 + else + tile_data[position] = nil + end + elseif(map_option == "flow") then + tile_data[position] = tileflags.flow_size elseif(map_option == "aquifer") then -- hardcoding these values bc they are not directly in a list if(tileflags.water_table and tile_occupancy.heavy_aquifer) then @@ -152,6 +168,17 @@ local function setup_keys(options) end end + if(options.liquid) then + KEYS.LIQUID = liquid_list + end + + if(options.flow) then + KEYS.FLOW = {} + for i=0, 7 do + KEYS.FLOW[i] = i + end + end + return KEYS end @@ -185,7 +212,7 @@ local function export_all_z_levels(fortress_name, folder, options) end -- start from bottom z-level (underworld) to top z-level (sky) - for z = 0, 1-1 do + for z = 25, 25 do --zmax do local level_data = {} for y = 0, ymax - 1 do local row_data = {} @@ -234,6 +261,8 @@ local options, args = { outside = false, aquifer = false, material = false, + flow = false, + liquid = false, }, {...} local positionals = argparse.processArgsGetopt(args, { @@ -248,6 +277,8 @@ local positionals = argparse.processArgsGetopt(args, { {'o', 'outside', handler=function() options.outside = true end}, {'a', 'aquifer', handler=function() options.aquifer = true end}, {'m', 'material', handler=function() options.material = true end}, + {'f', 'flow', handler=function() options.flow = true end}, + {'q', 'liquid', handler=function() options.liquid = true end}, -- local var since underworld not in ordered option {'u', 'underworld', handler=function() include_underworld_z = true end}, {'e', 'evilness', handler=function() evilness = get_evilness() end}, @@ -281,6 +312,8 @@ local ordered_options = { "outside", "aquifer", "material", + "flow", + "liquid", } -- reorganize ordered options based on selected options via argparse From acd44c7fdc198edac181ad441902b7de1cf8da3a Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:04:18 -0600 Subject: [PATCH 377/811] Fix zmin and zmax --- export-map.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/export-map.lua b/export-map.lua index a93c4230b8..7c3f1455a9 100644 --- a/export-map.lua +++ b/export-map.lua @@ -212,7 +212,7 @@ local function export_all_z_levels(fortress_name, folder, options) end -- start from bottom z-level (underworld) to top z-level (sky) - for z = 25, 25 do --zmax do + for z = zmin, zmax do local level_data = {} for y = 0, ymax - 1 do local row_data = {} From 901c9dc49d63132ddda7c73267b3bc1bef8384b7 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:10:40 -0600 Subject: [PATCH 378/811] Add export-map to changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 3809982b04..7bb0b8b829 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,7 @@ Template for new versions: ## New Tools - `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed. +- `export-map`: Export map tile data to a JSON file. ## New Features - `force`: support the ``Wildlife`` event to allow additional wildlife to enter the map From 747d20f35e011a76cdbb12571d1a2616dde36d8f Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:11:40 -0600 Subject: [PATCH 379/811] Remove map tag Co-authored-by: Myk --- docs/export-map.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/export-map.rst b/docs/export-map.rst index 11d1b87fbb..a77e8c8689 100644 --- a/docs/export-map.rst +++ b/docs/export-map.rst @@ -3,7 +3,7 @@ export-map .. dfhack-tool:: :summary: Export fortress map tile data to a JSON file - :tags: dev map + :tags: dev WARNING - This command will cause the game to freeze for minutes depending on map size and options enabled. From 188f7c93a1ebfa5821b86b6f2936f3b201f2b168 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:12:18 -0600 Subject: [PATCH 380/811] Change required arguments to parenthesis Co-authored-by: Myk --- docs/export-map.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/export-map.rst b/docs/export-map.rst index a77e8c8689..aa2ff1d4a0 100644 --- a/docs/export-map.rst +++ b/docs/export-map.rst @@ -18,7 +18,8 @@ Usage :: - export-map [include|exclude] [] + export-map + export-map (include|exclude) Examples -------- From 94a7df2384c71543f0ac2a71c6a90035e0059513 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:12:59 -0600 Subject: [PATCH 381/811] Change grammar and wording slightly Co-authored-by: Myk --- docs/export-map.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/export-map.rst b/docs/export-map.rst index aa2ff1d4a0..4bf79d154d 100644 --- a/docs/export-map.rst +++ b/docs/export-map.rst @@ -8,8 +8,8 @@ export-map WARNING - This command will cause the game to freeze for minutes depending on map size and options enabled. -Exports the fortress map tile data to a JSON file. (does not include items, -characters, buildings, etc.) Depending on options enabled, there will be a +Exports the fortress map tile data to a JSON file. The export does not include items, +characters, buildings, etc. Depending on options enabled, there will be a ``KEY`` table in the JSON with relevant [number ID] values that match a number to their object type. From c1857eedb6fd0c43ee99c946f04bb583e1857712 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:18:12 -0600 Subject: [PATCH 382/811] Change directory for export-map --- export-map.lua => devel/export-map.lua | 0 docs/{ => devel}/export-map.rst | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename export-map.lua => devel/export-map.lua (100%) rename docs/{ => devel}/export-map.rst (100%) diff --git a/export-map.lua b/devel/export-map.lua similarity index 100% rename from export-map.lua rename to devel/export-map.lua diff --git a/docs/export-map.rst b/docs/devel/export-map.rst similarity index 100% rename from docs/export-map.rst rename to docs/devel/export-map.rst From 041d415c9169157bc3e309721d9bb33150aea514 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:22:44 -0600 Subject: [PATCH 383/811] Add period to short summary --- docs/devel/export-map.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/devel/export-map.rst b/docs/devel/export-map.rst index 4bf79d154d..dd21e20e3a 100644 --- a/docs/devel/export-map.rst +++ b/docs/devel/export-map.rst @@ -2,7 +2,7 @@ export-map ========== .. dfhack-tool:: - :summary: Export fortress map tile data to a JSON file + :summary: Export fortress map tile data to a JSON file. :tags: dev WARNING - This command will cause the game to freeze for minutes depending on From 035a515d3badd24d7e23b6aa5eafed5326de2d8e Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:26:48 -0600 Subject: [PATCH 384/811] More grammar fixes --- docs/devel/export-map.rst | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/docs/devel/export-map.rst b/docs/devel/export-map.rst index dd21e20e3a..b9a1e11678 100644 --- a/docs/devel/export-map.rst +++ b/docs/devel/export-map.rst @@ -25,15 +25,15 @@ Examples -------- ``export-map`` - Exports the fortress map to JSON with ALL data included + Export the fortress map to JSON with ALL data included. ``export-map include -m -s -v`` - Exports the fortress map to JSON with only materials, shape, and variant - data included + Export the fortress map to JSON with only materials, shape, and variant + data included. ``export-map exclude --variant --hidden --light`` - Exports the fortress map to JSON with variant, hidden, and light data - excluded + Export the fortress map to JSON with variant, hidden, and light data + excluded. Required -------- @@ -41,10 +41,10 @@ Required When you are using options, you must include one of these settings. ``include`` - Include only the data listed from options to the JSON (whitelist) + Include only the data listed from options to the JSON (whitelist). ``exclude`` - Exclude only the data listed from options to the JSON (blacklist) + Exclude only the data listed from options to the JSON (blacklist). Options ------- @@ -53,27 +53,27 @@ Options Shows the help menu ``-t``, ``--tiletype`` - The tile material classification [number ID] (AIR/SOIL/STONE/RIVER/etc.) + The tile material classification. [number ID] (AIR/SOIL/STONE/RIVER/etc.) ``-s``, ``--shape`` - The tile shape classification [number ID] (EMPTY/FLOOR/WALL/STAIR/etc.) + The tile shape classification. [number ID] (EMPTY/FLOOR/WALL/STAIR/etc.) ``-p``, ``--special`` - The tile surface special properties for smoothness [number ID] - (NORMAL/SMOOTH/ROUGH/etc.) (used for engraving) + The tile surface special properties for smoothness. [number ID] + (NORMAL/SMOOTH/ROUGH/etc.) (used for engraving). ``-v``, ``--variant`` - The specific variant of a tile that have visual variations [number] (like + The specific variant of a tile that have visual variations. [number] (like grass tiles in ASCII mode) ``-h``, ``--hidden`` - Whether tile is revealed or unrevealed [boolean] + Whether tile is revealed or unrevealed. [boolean] ``-l``, ``--light`` - Whether tile is exposed to light [boolean] + Whether tile is exposed to light. [boolean] ``-b``, ``--subterranean`` - Whether the tile is considered underground [boolean] (used to determine + Whether the tile is considered underground. [boolean] (used to determine crops that can be planted underground) ``-o``, ``--outside`` @@ -81,14 +81,14 @@ Options to trigger on outside tiles) ``-a``, ``--aquifer`` - Whether the tile is considered an aquifer [number ID] (NONE/LIGHT/HEAVY) + Whether the tile is considered an aquifer. [number ID] (NONE/LIGHT/HEAVY) ``-m``, ``--material`` - The material inside the tile [number ID] (IRON/GRANITE/CLAY/ + The material inside the tile. [number ID] (IRON/GRANITE/CLAY/ TOPAZOLITE/BLACK_OPAL/etc.) (will return nil if the tile is empty) ``-u``, ``--underworld`` - Whether the underworld z-levels will be included + Whether the underworld z-levels will be included. ``-e``, ``--evilness`` Whether the evilness value will be included in MAP_SIZE table. This only @@ -99,7 +99,8 @@ JSON DATA --------- ``ARGUMENT_OPTION_ORDER`` - The order of the selected options for how data is arranged at a map position + The order of the selected options for how data is arranged at a map + position. Example 1: ``{"material": 1, "shape": 2, "hidden": 3}`` From e77e63c1dd5685d0dc7a4c2d5ecedb61b75899f8 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:29:36 -0600 Subject: [PATCH 385/811] Remove help documentation option --- docs/devel/export-map.rst | 3 --- 1 file changed, 3 deletions(-) diff --git a/docs/devel/export-map.rst b/docs/devel/export-map.rst index b9a1e11678..ac12be35ce 100644 --- a/docs/devel/export-map.rst +++ b/docs/devel/export-map.rst @@ -49,9 +49,6 @@ When you are using options, you must include one of these settings. Options ------- -``help``, ``--help`` - Shows the help menu - ``-t``, ``--tiletype`` The tile material classification. [number ID] (AIR/SOIL/STONE/RIVER/etc.) From 0082af75ec6e3d69bf3f023941279f6e9c417746 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:31:25 -0600 Subject: [PATCH 386/811] Remove redundant required section --- docs/devel/export-map.rst | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/docs/devel/export-map.rst b/docs/devel/export-map.rst index ac12be35ce..bbfa66f255 100644 --- a/docs/devel/export-map.rst +++ b/docs/devel/export-map.rst @@ -35,17 +35,6 @@ Examples Export the fortress map to JSON with variant, hidden, and light data excluded. -Required --------- - -When you are using options, you must include one of these settings. - -``include`` - Include only the data listed from options to the JSON (whitelist). - -``exclude`` - Exclude only the data listed from options to the JSON (blacklist). - Options ------- From 3bb8cffaa63e9a94c63277ea94d487a3c0d057ae Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:50:52 -0600 Subject: [PATCH 387/811] Add documentation for liquid and flow options --- docs/devel/export-map.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/devel/export-map.rst b/docs/devel/export-map.rst index bbfa66f255..7a306d6bbc 100644 --- a/docs/devel/export-map.rst +++ b/docs/devel/export-map.rst @@ -73,6 +73,13 @@ Options The material inside the tile. [number ID] (IRON/GRANITE/CLAY/ TOPAZOLITE/BLACK_OPAL/etc.) (will return nil if the tile is empty) +``-q``, ``--liquid`` + The type of liquid inside the tile. [number ID] (WATER/MAGMA) (will return + nil if the tile flow level is zero) + +``-f``, ``--flow`` + The level of liquids inside the tile. [number] (0-7) + ``-u``, ``--underworld`` Whether the underworld z-levels will be included. From 3f28dc5aebe9d2ef27675b2b2d4a98dc9ed7af87 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 02:51:08 -0600 Subject: [PATCH 388/811] Change liquid keys to be uppercase --- devel/export-map.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/export-map.lua b/devel/export-map.lua index 7c3f1455a9..12e1908f13 100644 --- a/devel/export-map.lua +++ b/devel/export-map.lua @@ -22,7 +22,7 @@ end -- right now the only tile_liquids are Water and Magma local liquid_list = {} for id, liquid in ipairs(df.tile_liquid) do - liquid_list[id] = liquid + liquid_list[id] = string.upper(liquid) end -- copied from agitation-rebalance.lua From dd6b878e28fa710514b1db01bc13e592bc2eada3 Mon Sep 17 00:00:00 2001 From: Tim Date: Sun, 2 Feb 2025 03:31:09 -0600 Subject: [PATCH 389/811] Remove fortress map desc --- devel/export-map.lua | 2 +- docs/devel/export-map.rst | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/devel/export-map.lua b/devel/export-map.lua index 12e1908f13..b690938d08 100644 --- a/devel/export-map.lua +++ b/devel/export-map.lua @@ -246,7 +246,7 @@ if dfhack_flags.module then end if not dfhack.isMapLoaded() then - qerror('This script requires a fortress map to be loaded') + qerror('This script requires a map to be loaded') end local options, args = { diff --git a/docs/devel/export-map.rst b/docs/devel/export-map.rst index 7a306d6bbc..6c4b15fa19 100644 --- a/docs/devel/export-map.rst +++ b/docs/devel/export-map.rst @@ -2,13 +2,13 @@ export-map ========== .. dfhack-tool:: - :summary: Export fortress map tile data to a JSON file. + :summary: Export map tile data to a JSON file. :tags: dev WARNING - This command will cause the game to freeze for minutes depending on map size and options enabled. -Exports the fortress map tile data to a JSON file. The export does not include items, +Exports the map tile data to a JSON file. The export does not include items, characters, buildings, etc. Depending on options enabled, there will be a ``KEY`` table in the JSON with relevant [number ID] values that match a number to their object type. @@ -25,14 +25,14 @@ Examples -------- ``export-map`` - Export the fortress map to JSON with ALL data included. + Export the map to JSON with ALL data included. ``export-map include -m -s -v`` - Export the fortress map to JSON with only materials, shape, and variant + Export the map to JSON with only materials, shape, and variant data included. ``export-map exclude --variant --hidden --light`` - Export the fortress map to JSON with variant, hidden, and light data + Export the map to JSON with variant, hidden, and light data excluded. Options From 39a7665e5d91c3bebce6c08fd314cd7980c2feda Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Sun, 2 Feb 2025 09:33:44 +0000 Subject: [PATCH 390/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- docs/devel/export-map.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/devel/export-map.rst b/docs/devel/export-map.rst index 6c4b15fa19..72032caa5e 100644 --- a/docs/devel/export-map.rst +++ b/docs/devel/export-map.rst @@ -74,7 +74,7 @@ Options TOPAZOLITE/BLACK_OPAL/etc.) (will return nil if the tile is empty) ``-q``, ``--liquid`` - The type of liquid inside the tile. [number ID] (WATER/MAGMA) (will return + The type of liquid inside the tile. [number ID] (WATER/MAGMA) (will return nil if the tile flow level is zero) ``-f``, ``--flow`` @@ -92,7 +92,7 @@ JSON DATA --------- ``ARGUMENT_OPTION_ORDER`` - The order of the selected options for how data is arranged at a map + The order of the selected options for how data is arranged at a map position. Example 1: From 1df503534580e3b523cf0f68736ba982b98e7c4e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 16:35:21 -0800 Subject: [PATCH 391/811] change hotkeys that conflict with EditField's Ctrl-a --- gui/launcher.lua | 4 ++-- gui/manipulator.lua | 2 +- internal/caravan/movegoods.lua | 2 +- internal/caravan/pedestal.lua | 2 +- internal/caravan/trade.lua | 4 ++-- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/gui/launcher.lua b/gui/launcher.lua index 9ea1c49c54..2ba8246ab1 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -261,7 +261,7 @@ function TagFilterPanel:init() widgets.HotkeyLabel{ frame={b=0, r=0}, label='Cycle all', - key='CUSTOM_CTRL_A', + key='CUSTOM_CTRL_N', auto_width=true, on_activate=self:callback('toggle_all') }, @@ -388,7 +388,7 @@ function AutocompletePanel:init() widgets.Label{ frame={l=0, t=3}, text={ - {key='CUSTOM_CTRL_W', key_sep=': ', on_activate=open_filter_panel, text='Tags:'}, + {key='CUSTOM_CTRL_F', key_sep=': ', on_activate=open_filter_panel, text='Tags:'}, {gap=1, pen=get_filter_pen, text=get_filter_text}, }, on_click=open_filter_panel, diff --git a/gui/manipulator.lua b/gui/manipulator.lua index 73cc885eda..f7135412dd 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -1057,7 +1057,7 @@ function QuickMenu:init() }, widgets.HotkeyLabel{ frame={b=0, l=0}, - key='CUSTOM_CTRL_A', + key='CUSTOM_CTRL_N', label='Select all', visible=self.multiselect, on_activate=function() diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index c935c5fb1d..df90d7f886 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -286,7 +286,7 @@ function MoveGoods:init() widgets.HotkeyLabel{ frame={l=0, b=0}, label='Select all/none', - key='CUSTOM_CTRL_A', + key='CUSTOM_CTRL_N', on_activate=self:callback('toggle_visible'), auto_width=true, }, diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index cd188914c1..363160093a 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -388,7 +388,7 @@ function AssignItems:init() widgets.HotkeyLabel{ frame={l=0, b=2}, label='Select all/none', - key='CUSTOM_CTRL_A', + key='CUSTOM_CTRL_N', on_activate=self:callback('toggle_visible'), auto_width=true, }, diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index 05e3adf3b1..d32db99456 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -309,7 +309,7 @@ function Trade:init() widgets.HotkeyLabel{ frame={l=0, b=0}, label='Select all/none', - key='CUSTOM_CTRL_A', + key='CUSTOM_CTRL_N', on_activate=self:callback('toggle_visible'), auto_width=true, }, @@ -891,7 +891,7 @@ function Ethics:init() }, widgets.HotkeyLabel{ frame={l=0, b=0}, - key='CUSTOM_CTRL_A', + key='CUSTOM_CTRL_N', label='Deselect items in trade list', auto_width=true, on_activate=self:callback('deselect_transgressions'), From e684f36a8870de0fedfecd7558725072fa7977aa Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 17:41:27 -0800 Subject: [PATCH 392/811] resolve more conflicts --- gui/civ-alert.lua | 2 +- internal/gm-unit/editor_civilization.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index ebdaa6662a..6e4b0cef79 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -218,7 +218,7 @@ function Civalert:init() }, widgets.HotkeyLabel{ frame={t=3, l=0}, - key='CUSTOM_CTRL_W', + key='CUSTOM_CTRL_N', label='Sound alarm! Citizens run to safety!', on_activate=sound_alarm, enabled=can_sound_alarm, diff --git a/internal/gm-unit/editor_civilization.lua b/internal/gm-unit/editor_civilization.lua index b1c6c0572a..2c6af1ca89 100644 --- a/internal/gm-unit/editor_civilization.lua +++ b/internal/gm-unit/editor_civilization.lua @@ -107,7 +107,7 @@ function CivBox:choose_race() end function CivBox:init(info) self.subviews.list.frame={t=3,r=0,l=0} - self.subviews.list.edit.ignore_keys={"STRING_A047"}, + self.subviews.list.edit.ignore_keys={"STRING_A047"} self:addviews{ widgets.Label{frame={t=1,l=0},text={ {text="Filter race ",key="STRING_A047",key_sep="()",on_activate=self:callback("choose_race")}, From 3a4ddc700c184f02430bb9a45f08f212e9e8662a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 17:43:36 -0800 Subject: [PATCH 393/811] absorb fixes from #1383 --- gui/quickfort.lua | 2 +- internal/gm-unit/editor_civilization.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/quickfort.lua b/gui/quickfort.lua index aff6824e00..3928cfb33c 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -38,7 +38,7 @@ transformations = transformations or {} -- blueprint selection dialog, shown when the script starts or when a user wants -- to load a new blueprint into the ui -BlueprintDialog = defclass(SelectDialog, gui.ZScreenModal) +BlueprintDialog = defclass(BlueprintDialog, gui.ZScreenModal) BlueprintDialog.ATTRS{ focus_path='quickfort/dialog', on_select=DEFAULT_NIL, diff --git a/internal/gm-unit/editor_civilization.lua b/internal/gm-unit/editor_civilization.lua index 2c6af1ca89..b12b00e62d 100644 --- a/internal/gm-unit/editor_civilization.lua +++ b/internal/gm-unit/editor_civilization.lua @@ -107,7 +107,7 @@ function CivBox:choose_race() end function CivBox:init(info) self.subviews.list.frame={t=3,r=0,l=0} - self.subviews.list.edit.ignore_keys={"STRING_A047"} + self.subviews.list.edit.text_area.text_area.ignore_keys={"STRING_A047"} self:addviews{ widgets.Label{frame={t=1,l=0},text={ {text="Filter race ",key="STRING_A047",key_sep="()",on_activate=self:callback("choose_race")}, From 191cdbeae99203b1aa38342b93df1bfa95946c46 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 18:00:36 -0800 Subject: [PATCH 394/811] fix comment and link to related bug --- internal/control-panel/registry.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 6b9213c9b0..37cd56c4e2 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -68,7 +68,8 @@ COMMANDS_BY_IDX = { -- bugfix tools {command='adamantine-cloth-wear', help_command='tweak', group='bugfix', mode='tweak', default=true, desc='Prevents adamantine clothing from wearing out while being worn.'}, - -- re-inserted below for non-Windows users (where the tweak doesn't work) + -- re-inserted below for non-Windows users (tweak can't load on Windows) + -- can be restored here once we solve issue #4292 -- {command='craft-age-wear', help_command='tweak', group='bugfix', mode='tweak', default=true, -- desc='Allows items crafted from organic materials to wear out over time.'}, {command='fix/blood-del', group='bugfix', mode='run', default=true}, From 55cacbb5128ae8f09e5f2b7fc20d71f5d25fda65 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 18:16:26 -0800 Subject: [PATCH 395/811] back out autocheese and the new advtools overlay to be reverted after the point release --- advtools.lua | 2 - autocheese.lua | 187 ---------------------------- changelog.txt | 2 - docs/advtools.rst | 13 -- docs/autocheese.rst | 39 ------ internal/control-panel/registry.lua | 2 - 6 files changed, 245 deletions(-) delete mode 100644 autocheese.lua delete mode 100644 docs/autocheese.rst diff --git a/advtools.lua b/advtools.lua index 9f4d2ec3b3..6b2b0d09af 100644 --- a/advtools.lua +++ b/advtools.lua @@ -1,12 +1,10 @@ --@ module=true local convo = reqscript('internal/advtools/convo') -local fastcombat = reqscript('internal/advtools/fastcombat') local party = reqscript('internal/advtools/party') OVERLAY_WIDGETS = { conversation=convo.AdvRumorsOverlay, - fastcombat=fastcombat.AdvCombatOverlay, } if dfhack_flags.module then diff --git a/autocheese.lua b/autocheese.lua deleted file mode 100644 index b365f3f7b1..0000000000 --- a/autocheese.lua +++ /dev/null @@ -1,187 +0,0 @@ ---@module = true - -local ic = reqscript('idle-crafting') - ----make cheese using a specific barrel and workshop ----@param barrel df.item ----@param workshop df.building_workshopst ----@return df.job -function makeCheese(barrel, workshop) - ---@type df.job - local job = ic.make_job() - job.job_type = df.job_type.MakeCheese - - local jitem = df.job_item:new() - jitem.quantity = 0 - jitem.vector_id = df.job_item_vector_id.ANY_COOKABLE - jitem.flags1.unrotten = true - jitem.flags1.milk = true - job.job_items.elements:insert('#', jitem) - - if not dfhack.job.attachJobItem(job, barrel, df.job_item_ref.T_role.Reagent, 0, -1) then - dfhack.error('could not attach item') - end - - ic.assignToWorkshop(job, workshop) - return job -end - - - ----unit is ready to take jobs ----@param unit df.unit ----@return boolean -function unitIsAvailable(unit) - if unit.job.current_job then - return false - elseif #unit.individual_drills > 0 then - return false - elseif unit.flags1.caged or unit.flags1.chained then - return false - elseif unit.military.squad_id ~= -1 then - local squad = df.squad.find(unit.military.squad_id) - -- this lookup should never fail - ---@diagnostic disable-next-line: need-check-nil - return #squad.orders == 0 and squad.activity == -1 - end - return true -end - ----check if unit can perform labor at workshop ----@param unit df.unit ----@param unit_labor df.unit_labor ----@param workshop df.building ----@return boolean -function availableLaborer(unit, unit_labor, workshop) - return unit.status.labors[unit_labor] - and unitIsAvailable(unit) - and ic.canAccessWorkshop(unit, workshop) -end - ----find unit with a particular labor enabled ----@param unit_labor df.unit_labor ----@param job_skill df.job_skill ----@param workshop df.building ----@return df.unit|nil ----@return integer|nil - function findAvailableLaborer(unit_labor, job_skill, workshop) - local max_unit = nil - local max_skill = -1 - for _, unit in ipairs(dfhack.units.getCitizens(true, false)) do - if - availableLaborer(unit, unit_labor, workshop) - then - local unit_skill = dfhack.units.getNominalSkill(unit, job_skill, true) - if unit_skill > max_skill then - max_unit = unit - max_skill = unit_skill - end - end - end - return max_unit, max_skill -end - -local function findMilkBarrel(min_liquids) - for _, container in ipairs(df.global.world.items.other.FOOD_STORAGE) do - if - not (container.flags.in_job or container.flags.forbid) and - container.flags.container and #container.general_refs >= min_liquids - then - local content_reference = dfhack.items.getGeneralRef(container, df.general_ref_type.CONTAINS_ITEM) - local contained_item = df.item.find(content_reference and content_reference.item_id or -1) - if contained_item then - local mat_info = dfhack.matinfo.decode(contained_item) - if mat_info:matches { milk = true } then - return container - end - end - end - end -end - ----find a workshop to which the barrel can be brought ----if the workshop has a master, only return workshop and master if the master is available ----@param pos df.coord ----@return df.building_workshopst? ----@return df.unit? -function findWorkshop(pos) - for _,workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do - if - dfhack.maps.canWalkBetween(pos, xyz2pos(workshop.centerx, workshop.centery, workshop.z)) and - not workshop.profile.blocked_labors[df.unit_labor.MAKE_CHEESE] and - #workshop.jobs == 0 - then - if #workshop.profile.permitted_workers == 0 then - -- immediately return workshop without master - return workshop, nil - else - unit = df.unit.find(workshop.profile.permitted_workers[0]) - if - unit and availableLaborer(unit, df.unit_labor.MAKE_CHEESE, workshop) - then - -- return workshop and master, if master is available - return workshop, unit - else - print("autocheese: Skipping farmer's workshop with unavailable master") - end - end - end - end -end - -if dfhack_flags.module then - return -end - --- actual script action - -local argparse = require('argparse') - -local min_number = 50 - -local _ = argparse.processArgsGetopt({...}, -{ - { 'm', 'min-milk', hasArg = true, - handler = function(min) - min_number = argparse.nonnegativeInt(min, 'min-milk') - end } -}) - - -local reagent = findMilkBarrel(min_number) - -if not reagent then - -- print('autocheese: no sufficiently full barrel found') - return -end - -local workshop, worker = findWorkshop(xyz2pos(dfhack.items.getPosition(reagent))) - -if not workshop then - print("autocheese: no Farmer's Workshop available") - return -end - --- try to find laborer for workshop without master -if not worker then - worker, _ = findAvailableLaborer(df.unit_labor.MAKE_CHEESE, df.job_skill.CHEESEMAKING, workshop) -end - -if not worker then - print('autocheese: no cheesemaker available') - return -end -local job = makeCheese(reagent, workshop) - -print(('autocheese: dispatching cheesemaking job for %s (%d milk) to %s'):format( - dfhack.df2console(dfhack.items.getReadableDescription(reagent)), - #reagent.general_refs, - dfhack.df2console(dfhack.units.getReadableName(worker)) -)) - - --- assign a worker and send it to fetch the barrel -dfhack.job.addWorker(job, worker) -dfhack.units.setPathGoal(worker, reagent.pos, df.unit_path_goal.GrabJobResources) -job.items[0].flags.is_fetching = true -job.flags.fetching = true diff --git a/changelog.txt b/changelog.txt index 1de9ccde64..183b657f1f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,10 +27,8 @@ Template for new versions: # Future ## New Tools -- `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk ## New Features -- `advtools`: new overlay ``advtools.fastcombat``; allows you to skip combat animations and the announcement "More" button ## Fixes - `advtools`: fix dfhack-added conversation options not appearing in the ask whereabouts conversation tree diff --git a/docs/advtools.rst b/docs/advtools.rst index 113d6c969a..c62cdb1f83 100644 --- a/docs/advtools.rst +++ b/docs/advtools.rst @@ -35,16 +35,3 @@ enemies will gain the ``slay`` and ``kill`` keywords. It will also add additional conversation options for asking whereabouts of your relationships -- in vanilla, you can only ask whereabouts of historical figures involved in rumors you personally witnessed or heard about. - -``advtools.fastcombat`` -~~~~~~~~~~~~~~~~~~~~~~~ - -When enabled, this overlay will allow you to skip most combat animations, -including the whooshes and projectiles travelling through the screen. It will -also let you skip the announcements window when the "More" button is active, -scrolling you to the very bottom with the first press, and skipping the window -entirely with the second press. This drastically speeds up combat while still -giving you the option not to skip the announcements. Skip keys are left mouse click, -the SELECT button, the movement keys and combat-related keys that don't bring up a -menu (such as bump attack). If clicking to skip past combat, it will only skip the -announcements if you're clicking outside the announcements panel. diff --git a/docs/autocheese.rst b/docs/autocheese.rst deleted file mode 100644 index fb92874df5..0000000000 --- a/docs/autocheese.rst +++ /dev/null @@ -1,39 +0,0 @@ -autocheese -========== - -.. dfhack-tool:: - :summary: Schedule cheese making jobs based on milk reserves. - :tags: fort auto - -Cheese making is difficult to automate using work orders. A single job -can consume anything from a bucket with a single unit of milk to a barrel -with 100 units of milk. This makes it hard to predict how much cheese will -actually be produced by an automated order. - -The script will scan your fort for barrels with a certain minimum amount of milk -(default: 50), create a cheese making job specifically for that barrel, and -assign this job to one of your idle dwarves (giving preference to skilled cheese -makers). - -When enabled using `gui/control-panel`, the script will run automatically, with -default options, twice a month. - -Usage ------ - -:: - - autocheese [] - -Examples --------- - -``autocheese -m 100`` - Only create a job if there is a barrel that is filled to the maximum. - -Options -------- - -``-m``, ``--min-milk`` - Set the minimum number of milk items in a barrel for the barrel to be - considered for cheese making. diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 37cd56c4e2..b0e6eee753 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -18,8 +18,6 @@ COMMANDS_BY_IDX = { {command='autobutcher target 10 10 14 2 BIRD_PEAFOWL_BLUE', group='automation', mode='run', desc='Enable if you usually want to raise peafowl.'}, {command='autochop', group='automation', mode='enable'}, - {command='autocheese', group='automation', mode='repeat', - params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'autocheese', ']'}}, {command='autoclothing', group='automation', mode='enable'}, {command='autofarm', group='automation', mode='enable'}, {command='autofarm threshold 150 grass_tail_pig', group='automation', mode='run', From 9e9d64f210b6f724ed992e594dd0bca29fa22ec4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 18:16:47 -0800 Subject: [PATCH 396/811] Revert "hide popups for adventure mode; add reset command" This reverts commit 7546d3c3bdb3780963948b319d5c27bd2347d8e3. --- changelog.txt | 2 -- docs/hide-tutorials.rst | 14 ++++---------- hide-tutorials.lua | 37 +++++++++---------------------------- 3 files changed, 13 insertions(+), 40 deletions(-) diff --git a/changelog.txt b/changelog.txt index 183b657f1f..a3155dcd2c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,8 +37,6 @@ Template for new versions: ## Misc Improvements - `assign-preferences`: new ``--show`` option to display the preferences of the selected unit - `pref-adjust`: new ``show`` command to display the preferences of the selected unit -- `hide-tutorials`: if enabled, also hide tutorial popups for adventure mode -- `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game ## Removed - `gui/control-panel`: removed ``craft-age-wear`` tweak for Windows users; the tweak doesn't currently load on Windows diff --git a/docs/hide-tutorials.rst b/docs/hide-tutorials.rst index 94b2a27a28..417d5e278c 100644 --- a/docs/hide-tutorials.rst +++ b/docs/hide-tutorials.rst @@ -3,12 +3,12 @@ hide-tutorials .. dfhack-tool:: :summary: Hide new fort tutorial popups. - :tags: adventure fort interface + :tags: fort interface If you've played the game before and don't need to see the tutorial popups that show up on every new fort, ``hide-tutorials`` can hide them for you. You can enable this tool as a system service in the "Services" tab of -`gui/control-panel` so it takes effect for all forts and adventures. +`gui/control-panel` so it takes effect for all new or loaded forts. Specifically, this tool hides: @@ -16,8 +16,6 @@ Specifically, this tool hides: - The "Do you want to start a tutorial embark" popup - Popups displayed the first time you open the labor, burrows, justice, and other similar screens in a new fort -- Popups displayed when you perform certain actions for the first time in an - adventure Note that only unsolicited tutorial popups are hidden. If you directly request a tutorial page from the help, then it will still function normally. @@ -29,10 +27,6 @@ Usage enable hide-tutorials hide-tutorials - hide-tutorials reset -If you haven't enabled the tool, but you run the command while a fort or -adventure is loaded, all future popups for the loaded game will be hidden. - -If you run the command with the ``reset`` option, all popups will be re-enabled -as if they had never been seen or dismissed. +If you haven't enabled the tool, but you run the command while a fort is +loaded, all future popups for the loaded fort will be hidden. diff --git a/hide-tutorials.lua b/hide-tutorials.lua index e6124de0de..2ca950e3bf 100644 --- a/hide-tutorials.lua +++ b/hide-tutorials.lua @@ -12,6 +12,10 @@ function isEnabled() return enabled end +local function is_fort_map_loaded() + return df.global.gamemode == df.game_mode.DWARF and dfhack.isMapLoaded() +end + local help = df.global.game.main_interface.help local function close_help() @@ -39,36 +43,15 @@ function skip_tutorial_prompt() end end -local function get_prefix() - if dfhack.world.isFortressMode() then - return 'POPUP_' - elseif dfhack.world.isAdventureMode() then - return 'ADVENTURE_POPUP_' - end -end - local function hide_all_popups() - local prefix = get_prefix() - if not prefix then return end for i,name in ipairs(df.help_context_type) do - if not name:startswith(prefix) then goto continue end + if not name:startswith('POPUP_') then goto continue end utils.insert_sorted(df.global.plotinfo.tutorial_seen, i) utils.insert_sorted(df.global.plotinfo.tutorial_hide, i) ::continue:: end end -local function show_all_popups() - local prefix = get_prefix() - if not prefix then return end - for i,name in ipairs(df.help_context_type) do - if not name:startswith(prefix) then goto continue end - utils.erase_sorted(df.global.plotinfo.tutorial_seen, i) - utils.erase_sorted(df.global.plotinfo.tutorial_hide, i) - ::continue:: - end -end - dfhack.onStateChange[GLOBAL_KEY] = function(sc) if not enabled then return end @@ -82,7 +65,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) dfhack.timeout(100, 'frames', skip_tutorial_prompt) dfhack.timeout(1000, 'frames', skip_tutorial_prompt) end - elseif sc == SC_MAP_LOADED then + elseif sc == SC_MAP_LOADED and df.global.gamemode == df.game_mode.DWARF then hide_all_popups() end end @@ -98,15 +81,13 @@ end if args[1] == "enable" then enabled = true - if dfhack.isMapLoaded() then + if is_fort_map_loaded() then hide_all_popups() end elseif args[1] == "disable" then enabled = false -elseif args[1] == "reset" then - show_all_popups() -elseif dfhack.isMapLoaded() then +elseif is_fort_map_loaded() then hide_all_popups() else - qerror('hide-tutorials needs a loaded fortress or adventure map to work') + qerror('hide-tutorials needs a loaded fortress map to work') end From c4d19c15432b6ca85420580519f491380bbfac3a Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 2 Feb 2025 18:29:56 -0800 Subject: [PATCH 397/811] Revert "temporarily back out new features" --- advtools.lua | 2 + autocheese.lua | 187 ++++++++++++++++++++++++++++ changelog.txt | 4 + docs/advtools.rst | 13 ++ docs/autocheese.rst | 39 ++++++ docs/hide-tutorials.rst | 14 ++- hide-tutorials.lua | 37 ++++-- internal/control-panel/registry.lua | 2 + 8 files changed, 285 insertions(+), 13 deletions(-) create mode 100644 autocheese.lua create mode 100644 docs/autocheese.rst diff --git a/advtools.lua b/advtools.lua index 6b2b0d09af..9f4d2ec3b3 100644 --- a/advtools.lua +++ b/advtools.lua @@ -1,10 +1,12 @@ --@ module=true local convo = reqscript('internal/advtools/convo') +local fastcombat = reqscript('internal/advtools/fastcombat') local party = reqscript('internal/advtools/party') OVERLAY_WIDGETS = { conversation=convo.AdvRumorsOverlay, + fastcombat=fastcombat.AdvCombatOverlay, } if dfhack_flags.module then diff --git a/autocheese.lua b/autocheese.lua new file mode 100644 index 0000000000..b365f3f7b1 --- /dev/null +++ b/autocheese.lua @@ -0,0 +1,187 @@ +--@module = true + +local ic = reqscript('idle-crafting') + +---make cheese using a specific barrel and workshop +---@param barrel df.item +---@param workshop df.building_workshopst +---@return df.job +function makeCheese(barrel, workshop) + ---@type df.job + local job = ic.make_job() + job.job_type = df.job_type.MakeCheese + + local jitem = df.job_item:new() + jitem.quantity = 0 + jitem.vector_id = df.job_item_vector_id.ANY_COOKABLE + jitem.flags1.unrotten = true + jitem.flags1.milk = true + job.job_items.elements:insert('#', jitem) + + if not dfhack.job.attachJobItem(job, barrel, df.job_item_ref.T_role.Reagent, 0, -1) then + dfhack.error('could not attach item') + end + + ic.assignToWorkshop(job, workshop) + return job +end + + + +---unit is ready to take jobs +---@param unit df.unit +---@return boolean +function unitIsAvailable(unit) + if unit.job.current_job then + return false + elseif #unit.individual_drills > 0 then + return false + elseif unit.flags1.caged or unit.flags1.chained then + return false + elseif unit.military.squad_id ~= -1 then + local squad = df.squad.find(unit.military.squad_id) + -- this lookup should never fail + ---@diagnostic disable-next-line: need-check-nil + return #squad.orders == 0 and squad.activity == -1 + end + return true +end + +---check if unit can perform labor at workshop +---@param unit df.unit +---@param unit_labor df.unit_labor +---@param workshop df.building +---@return boolean +function availableLaborer(unit, unit_labor, workshop) + return unit.status.labors[unit_labor] + and unitIsAvailable(unit) + and ic.canAccessWorkshop(unit, workshop) +end + +---find unit with a particular labor enabled +---@param unit_labor df.unit_labor +---@param job_skill df.job_skill +---@param workshop df.building +---@return df.unit|nil +---@return integer|nil + function findAvailableLaborer(unit_labor, job_skill, workshop) + local max_unit = nil + local max_skill = -1 + for _, unit in ipairs(dfhack.units.getCitizens(true, false)) do + if + availableLaborer(unit, unit_labor, workshop) + then + local unit_skill = dfhack.units.getNominalSkill(unit, job_skill, true) + if unit_skill > max_skill then + max_unit = unit + max_skill = unit_skill + end + end + end + return max_unit, max_skill +end + +local function findMilkBarrel(min_liquids) + for _, container in ipairs(df.global.world.items.other.FOOD_STORAGE) do + if + not (container.flags.in_job or container.flags.forbid) and + container.flags.container and #container.general_refs >= min_liquids + then + local content_reference = dfhack.items.getGeneralRef(container, df.general_ref_type.CONTAINS_ITEM) + local contained_item = df.item.find(content_reference and content_reference.item_id or -1) + if contained_item then + local mat_info = dfhack.matinfo.decode(contained_item) + if mat_info:matches { milk = true } then + return container + end + end + end + end +end + +---find a workshop to which the barrel can be brought +---if the workshop has a master, only return workshop and master if the master is available +---@param pos df.coord +---@return df.building_workshopst? +---@return df.unit? +function findWorkshop(pos) + for _,workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do + if + dfhack.maps.canWalkBetween(pos, xyz2pos(workshop.centerx, workshop.centery, workshop.z)) and + not workshop.profile.blocked_labors[df.unit_labor.MAKE_CHEESE] and + #workshop.jobs == 0 + then + if #workshop.profile.permitted_workers == 0 then + -- immediately return workshop without master + return workshop, nil + else + unit = df.unit.find(workshop.profile.permitted_workers[0]) + if + unit and availableLaborer(unit, df.unit_labor.MAKE_CHEESE, workshop) + then + -- return workshop and master, if master is available + return workshop, unit + else + print("autocheese: Skipping farmer's workshop with unavailable master") + end + end + end + end +end + +if dfhack_flags.module then + return +end + +-- actual script action + +local argparse = require('argparse') + +local min_number = 50 + +local _ = argparse.processArgsGetopt({...}, +{ + { 'm', 'min-milk', hasArg = true, + handler = function(min) + min_number = argparse.nonnegativeInt(min, 'min-milk') + end } +}) + + +local reagent = findMilkBarrel(min_number) + +if not reagent then + -- print('autocheese: no sufficiently full barrel found') + return +end + +local workshop, worker = findWorkshop(xyz2pos(dfhack.items.getPosition(reagent))) + +if not workshop then + print("autocheese: no Farmer's Workshop available") + return +end + +-- try to find laborer for workshop without master +if not worker then + worker, _ = findAvailableLaborer(df.unit_labor.MAKE_CHEESE, df.job_skill.CHEESEMAKING, workshop) +end + +if not worker then + print('autocheese: no cheesemaker available') + return +end +local job = makeCheese(reagent, workshop) + +print(('autocheese: dispatching cheesemaking job for %s (%d milk) to %s'):format( + dfhack.df2console(dfhack.items.getReadableDescription(reagent)), + #reagent.general_refs, + dfhack.df2console(dfhack.units.getReadableName(worker)) +)) + + +-- assign a worker and send it to fetch the barrel +dfhack.job.addWorker(job, worker) +dfhack.units.setPathGoal(worker, reagent.pos, df.unit_path_goal.GrabJobResources) +job.items[0].flags.is_fetching = true +job.flags.fetching = true diff --git a/changelog.txt b/changelog.txt index a3155dcd2c..1de9ccde64 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,8 +27,10 @@ Template for new versions: # Future ## New Tools +- `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk ## New Features +- `advtools`: new overlay ``advtools.fastcombat``; allows you to skip combat animations and the announcement "More" button ## Fixes - `advtools`: fix dfhack-added conversation options not appearing in the ask whereabouts conversation tree @@ -37,6 +39,8 @@ Template for new versions: ## Misc Improvements - `assign-preferences`: new ``--show`` option to display the preferences of the selected unit - `pref-adjust`: new ``show`` command to display the preferences of the selected unit +- `hide-tutorials`: if enabled, also hide tutorial popups for adventure mode +- `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game ## Removed - `gui/control-panel`: removed ``craft-age-wear`` tweak for Windows users; the tweak doesn't currently load on Windows diff --git a/docs/advtools.rst b/docs/advtools.rst index c62cdb1f83..113d6c969a 100644 --- a/docs/advtools.rst +++ b/docs/advtools.rst @@ -35,3 +35,16 @@ enemies will gain the ``slay`` and ``kill`` keywords. It will also add additional conversation options for asking whereabouts of your relationships -- in vanilla, you can only ask whereabouts of historical figures involved in rumors you personally witnessed or heard about. + +``advtools.fastcombat`` +~~~~~~~~~~~~~~~~~~~~~~~ + +When enabled, this overlay will allow you to skip most combat animations, +including the whooshes and projectiles travelling through the screen. It will +also let you skip the announcements window when the "More" button is active, +scrolling you to the very bottom with the first press, and skipping the window +entirely with the second press. This drastically speeds up combat while still +giving you the option not to skip the announcements. Skip keys are left mouse click, +the SELECT button, the movement keys and combat-related keys that don't bring up a +menu (such as bump attack). If clicking to skip past combat, it will only skip the +announcements if you're clicking outside the announcements panel. diff --git a/docs/autocheese.rst b/docs/autocheese.rst new file mode 100644 index 0000000000..fb92874df5 --- /dev/null +++ b/docs/autocheese.rst @@ -0,0 +1,39 @@ +autocheese +========== + +.. dfhack-tool:: + :summary: Schedule cheese making jobs based on milk reserves. + :tags: fort auto + +Cheese making is difficult to automate using work orders. A single job +can consume anything from a bucket with a single unit of milk to a barrel +with 100 units of milk. This makes it hard to predict how much cheese will +actually be produced by an automated order. + +The script will scan your fort for barrels with a certain minimum amount of milk +(default: 50), create a cheese making job specifically for that barrel, and +assign this job to one of your idle dwarves (giving preference to skilled cheese +makers). + +When enabled using `gui/control-panel`, the script will run automatically, with +default options, twice a month. + +Usage +----- + +:: + + autocheese [] + +Examples +-------- + +``autocheese -m 100`` + Only create a job if there is a barrel that is filled to the maximum. + +Options +------- + +``-m``, ``--min-milk`` + Set the minimum number of milk items in a barrel for the barrel to be + considered for cheese making. diff --git a/docs/hide-tutorials.rst b/docs/hide-tutorials.rst index 417d5e278c..94b2a27a28 100644 --- a/docs/hide-tutorials.rst +++ b/docs/hide-tutorials.rst @@ -3,12 +3,12 @@ hide-tutorials .. dfhack-tool:: :summary: Hide new fort tutorial popups. - :tags: fort interface + :tags: adventure fort interface If you've played the game before and don't need to see the tutorial popups that show up on every new fort, ``hide-tutorials`` can hide them for you. You can enable this tool as a system service in the "Services" tab of -`gui/control-panel` so it takes effect for all new or loaded forts. +`gui/control-panel` so it takes effect for all forts and adventures. Specifically, this tool hides: @@ -16,6 +16,8 @@ Specifically, this tool hides: - The "Do you want to start a tutorial embark" popup - Popups displayed the first time you open the labor, burrows, justice, and other similar screens in a new fort +- Popups displayed when you perform certain actions for the first time in an + adventure Note that only unsolicited tutorial popups are hidden. If you directly request a tutorial page from the help, then it will still function normally. @@ -27,6 +29,10 @@ Usage enable hide-tutorials hide-tutorials + hide-tutorials reset -If you haven't enabled the tool, but you run the command while a fort is -loaded, all future popups for the loaded fort will be hidden. +If you haven't enabled the tool, but you run the command while a fort or +adventure is loaded, all future popups for the loaded game will be hidden. + +If you run the command with the ``reset`` option, all popups will be re-enabled +as if they had never been seen or dismissed. diff --git a/hide-tutorials.lua b/hide-tutorials.lua index 2ca950e3bf..e6124de0de 100644 --- a/hide-tutorials.lua +++ b/hide-tutorials.lua @@ -12,10 +12,6 @@ function isEnabled() return enabled end -local function is_fort_map_loaded() - return df.global.gamemode == df.game_mode.DWARF and dfhack.isMapLoaded() -end - local help = df.global.game.main_interface.help local function close_help() @@ -43,15 +39,36 @@ function skip_tutorial_prompt() end end +local function get_prefix() + if dfhack.world.isFortressMode() then + return 'POPUP_' + elseif dfhack.world.isAdventureMode() then + return 'ADVENTURE_POPUP_' + end +end + local function hide_all_popups() + local prefix = get_prefix() + if not prefix then return end for i,name in ipairs(df.help_context_type) do - if not name:startswith('POPUP_') then goto continue end + if not name:startswith(prefix) then goto continue end utils.insert_sorted(df.global.plotinfo.tutorial_seen, i) utils.insert_sorted(df.global.plotinfo.tutorial_hide, i) ::continue:: end end +local function show_all_popups() + local prefix = get_prefix() + if not prefix then return end + for i,name in ipairs(df.help_context_type) do + if not name:startswith(prefix) then goto continue end + utils.erase_sorted(df.global.plotinfo.tutorial_seen, i) + utils.erase_sorted(df.global.plotinfo.tutorial_hide, i) + ::continue:: + end +end + dfhack.onStateChange[GLOBAL_KEY] = function(sc) if not enabled then return end @@ -65,7 +82,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) dfhack.timeout(100, 'frames', skip_tutorial_prompt) dfhack.timeout(1000, 'frames', skip_tutorial_prompt) end - elseif sc == SC_MAP_LOADED and df.global.gamemode == df.game_mode.DWARF then + elseif sc == SC_MAP_LOADED then hide_all_popups() end end @@ -81,13 +98,15 @@ end if args[1] == "enable" then enabled = true - if is_fort_map_loaded() then + if dfhack.isMapLoaded() then hide_all_popups() end elseif args[1] == "disable" then enabled = false -elseif is_fort_map_loaded() then +elseif args[1] == "reset" then + show_all_popups() +elseif dfhack.isMapLoaded() then hide_all_popups() else - qerror('hide-tutorials needs a loaded fortress map to work') + qerror('hide-tutorials needs a loaded fortress or adventure map to work') end diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index b0e6eee753..37cd56c4e2 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -18,6 +18,8 @@ COMMANDS_BY_IDX = { {command='autobutcher target 10 10 14 2 BIRD_PEAFOWL_BLUE', group='automation', mode='run', desc='Enable if you usually want to raise peafowl.'}, {command='autochop', group='automation', mode='enable'}, + {command='autocheese', group='automation', mode='repeat', + params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'autocheese', ']'}}, {command='autoclothing', group='automation', mode='enable'}, {command='autofarm', group='automation', mode='enable'}, {command='autofarm threshold 150 grass_tail_pig', group='automation', mode='run', From b1aa3b365d611432195b80d1d064c942f54a78c7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 2 Feb 2025 18:34:42 -0800 Subject: [PATCH 398/811] bump changelog to 51.04-r1.1 --- changelog.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/changelog.txt b/changelog.txt index a3155dcd2c..c973300d0a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,14 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Removed + +# 51.04-r1.1 + ## Fixes - `advtools`: fix dfhack-added conversation options not appearing in the ask whereabouts conversation tree - `gui/rename`: fix error when changing the language of a unit's name From 6fa6ac367717ad337aa5fb323ba8f733f61457e1 Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 2 Feb 2025 19:32:13 -0800 Subject: [PATCH 399/811] Update changelog.txt --- changelog.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index bbe00f461e..887c3075b5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,8 @@ Template for new versions: ## Fixes ## Misc Improvements +- `hide-tutorials`: if enabled, also hide tutorial popups for adventure mode +- `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game ## Removed @@ -47,8 +49,6 @@ Template for new versions: ## Misc Improvements - `assign-preferences`: new ``--show`` option to display the preferences of the selected unit - `pref-adjust`: new ``show`` command to display the preferences of the selected unit -- `hide-tutorials`: if enabled, also hide tutorial popups for adventure mode -- `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game ## Removed - `gui/control-panel`: removed ``craft-age-wear`` tweak for Windows users; the tweak doesn't currently load on Windows From 9338977b7965c5a5d13020ddc1d4bd555affce95 Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 3 Feb 2025 02:40:49 -0600 Subject: [PATCH 400/811] Add JSON zero index disclaimer --- docs/devel/export-map.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/devel/export-map.rst b/docs/devel/export-map.rst index 72032caa5e..742d9df60f 100644 --- a/docs/devel/export-map.rst +++ b/docs/devel/export-map.rst @@ -130,11 +130,12 @@ JSON DATA ``map`` JSON map data is arranged as: ``map[z][y][x] = {tile_data}`` - JSON maps start at index [1]. (starts at map[1][1][1]) DF maps start at index [0]. (starts at map[0][0][0]) - To translate an actual DF map position from the JSON map you need add +1 to - all x/y/z coordinates to get the correct tile position. + For most JSON libraries the index starts at [0] but some languages has the + index start at [1]. So to translate an actual DF map position from the JSON + map, you may need to add +1 to all x/y/z coordinates to get the correct tile + position. The ``ARGUMENT_OPTION_ORDER`` determines order of tile data. (see above) I would recommend referencing the tile data like so: From 04527c097219451c07a6183104ab4c97e7ae69a4 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:27:57 -0800 Subject: [PATCH 401/811] add stub gui/spectate --- docs/gui/spectate.rst | 16 ++++++++++++++++ gui/spectate.lua | 31 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+) create mode 100644 docs/gui/spectate.rst create mode 100644 gui/spectate.lua diff --git a/docs/gui/spectate.rst b/docs/gui/spectate.rst new file mode 100644 index 0000000000..e6034cbf80 --- /dev/null +++ b/docs/gui/spectate.rst @@ -0,0 +1,16 @@ +gui/spectate +============ + +.. dfhack-tool:: + :summary: Automated spectator mode. + :tags: fort inspection interface + +This is an in-game configuration interface for `spectate`, which automatically +sets the camera to follow interesting units. + +Usage +----- + +:: + + gui/spectate diff --git a/gui/spectate.lua b/gui/spectate.lua new file mode 100644 index 0000000000..4cea75855d --- /dev/null +++ b/gui/spectate.lua @@ -0,0 +1,31 @@ +local gui = require('gui') +local spectate = require('plugins.spectate') +local widgets = require('gui.widgets') + +Spectate = defclass(Spectate, widgets.Window) +Spectate.ATTRS { + frame_title='Spectate', + frame={w=50, h=45}, + resizable=true, + resize_min={w=50, h=20}, +} + +function Spectate:init() + self:addviews{ + } +end + +SpectateScreen = defclass(SpectateScreen, gui.ZScreen) +SpectateScreen.ATTRS { + focus_path='spectate', +} + +function SpectateScreen:init() + self:addviews{Spectate{}} +end + +function SpectateScreen:onDismiss() + view = nil +end + +view = view and view:raise() or SpectateScreen{}:show() From b6e8e30fb5f98d1f1925f988675471a44a464375 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 3 Feb 2025 04:35:36 -0800 Subject: [PATCH 402/811] add changelog for #1392 --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 887c3075b5..c433ef6fe6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,7 @@ Template for new versions: ## New Tools - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk +- `gui/spectate`: interactive UI for configuring `spectate` ## New Features - `advtools`: new overlay ``advtools.fastcombat``; allows you to skip combat animations and the announcement "More" button From 09efb1af555a619fef02993cc3e115a9c1cbc93c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Feb 2025 22:19:01 +0000 Subject: [PATCH 403/811] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.30.0 → 0.31.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.30.0...0.31.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 35ecd16a6b..66e3d0f1cb 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.30.0 + rev: 0.31.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 9fff5195779da60dd85bcd622de1dabe966a3cb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 6 Feb 2025 08:23:08 +0100 Subject: [PATCH 404/811] Remove redundant journal tests This tests has been moved to TextArea widget where they belongs --- test/gui/journal.lua | 2465 +----------------------------------------- 1 file changed, 6 insertions(+), 2459 deletions(-) diff --git a/test/gui/journal.lua b/test/gui/journal.lua index d86e415d10..19975a7379 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -95,8 +95,7 @@ local function arrange_empty_journal(options) journal_window.frame.h = options.h + 6 end - - local text_area = journal_window.subviews.text_area + local text_area = journal_window.subviews.journal_editor.text_area text_area.enable_cursor_blink = false if not options.save_on_change then @@ -167,2042 +166,12 @@ local function read_selected_text(text_area) end function test.load() - local journal, text_area = arrange_empty_journal() - text_area:setText(' ') - journal:onRender() - - expect.eq('dfhack/lua/journal', dfhack.gui.getCurFocus(true)[1]) - expect.eq(read_rendered_text(text_area), '_') - - journal:dismiss() -end - -function test.load_input_multiline_text() - local journal, text_area, journal_window = arrange_empty_journal({w=80}) - - local text = table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'Pellentesque dignissim volutpat orci, sed molestie metus elementum vel.', - 'Donec sit amet mattis ligula, ac vestibulum lorem.', - }, '\n') - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), text .. '_') - - journal:dismiss() -end - -function test.handle_numpad_numbers_as_text() - local journal, text_area, journal_window = arrange_empty_journal({w=80}) - - local text = table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - simulate_input_text(text) - - simulate_input_keys({ - STANDARDSCROLL_LEFT = true, - KEYBOARD_CURSOR_LEFT = true, - _STRING = 52, - STRING_A052 = true, - }) - - expect.eq(read_rendered_text(text_area), text .. '4_') - - simulate_input_keys({ - STRING_A054 = true, - STANDARDSCROLL_RIGHT = true, - KEYBOARD_CURSOR_RIGHT = true, - _STRING = 54, - }) - expect.eq(read_rendered_text(text_area), text .. '46_') - - simulate_input_keys({ - KEYBOARD_CURSOR_DOWN = true, - STRING_A050 = true, - _STRING = 50, - STANDARDSCROLL_DOWN = true, - }) - - expect.eq(read_rendered_text(text_area), text .. '462_') - - simulate_input_keys({ - KEYBOARD_CURSOR_UP = true, - STRING_A056 = true, - STANDARDSCROLL_UP = true, - _STRING = 56, - }) - - expect.eq(read_rendered_text(text_area), text .. '4628_') - journal:dismiss() -end - -function test.wrap_text_to_available_width() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor est pellentesque ac.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac._', - }, '\n')); - - journal:dismiss() -end - -function test.submit_new_line() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('SELECT') - simulate_input_keys('SELECT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '', - '_', - }, '\n')); - - text_area:setCursor(58) - journal:onRender() - - simulate_input_keys('SELECT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'el', - '_t.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - -- empty end lines are not rendered - }, '\n')); - - text_area:setCursor(84) - journal:onRender() - - simulate_input_keys('SELECT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'el', - 'it.', - '112: Sed consectetur,', - -- wrapping changed - '_urna sit amet aliquet egestas, ante nibh porttitor ', - 'mi, vitae rutrum eros metus nec libero.', - -- empty end lines are not rendered - }, '\n')); - - journal:dismiss() -end - -function test.keyboard_arrow_up_navigation() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor est pellentesque ac.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim _uismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim li_ero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP') - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor _i, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP') - simulate_input_keys('KEYBOARD_CURSOR_UP') - simulate_input_keys('KEYBOARD_CURSOR_UP') - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur_ urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - journal:dismiss() -end - -function test.keyboard_arrow_down_navigation() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor est pellentesque ac.', - }, '\n') - - simulate_input_text(text) - text_area:setCursor(11) - journal:onRender() - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem _psum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit._', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed c_nsectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellen_esque ac.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin dignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac._', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - '41: Etiam id congue urna, vel aliquet mi.', - '45: Nam dignissim libero a interdum porttitor.', - '73: Proin _ignissim euismod augue, laoreet porttitor ', - 'est pellentesque ac.', - }, '\n')); - - journal:dismiss() -end - -function test.keyboard_arrow_left_navigation() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero_', - }, '\n')); - - for i=1,6 do - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - '_ibero.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec_', - 'libero.', - }, '\n')); - - for i=1,105 do - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit._', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,60 do - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - journal:dismiss() -end - -function test.keyboard_arrow_right_navigation() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '6_: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,53 do - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing_', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - '_lit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,5 do - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit._', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,113 do - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - }, '\n')); - - journal:dismiss() -end - -function test.handle_backspace() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('STRING_A000') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero_', - }, '\n')); - - for i=1,3 do - simulate_input_keys('STRING_A000') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec lib_', - }, '\n')); - - text_area:setCursor(62) - journal:onRender() - - simulate_input_keys('STRING_A000') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit._12: Sed consectetur, urna sit amet aliquet ', - 'egestas, ante nibh porttitor mi, vitae rutrum eros ', - 'metus nec lib', - }, '\n')); - - text_area:setCursor(2) - journal:onRender() - - simulate_input_keys('STRING_A000') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.112: Sed consectetur, urna sit amet aliquet ', - 'egestas, ante nibh porttitor mi, vitae rutrum eros ', - 'metus nec lib', - }, '\n')); - - simulate_input_keys('STRING_A000') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.112: Sed consectetur, urna sit amet aliquet ', - 'egestas, ante nibh porttitor mi, vitae rutrum eros ', - 'metus nec lib', - }, '\n')); - - journal:dismiss() -end - -function test.handle_delete() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(124) - journal:onRender() - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - '_rttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(123) - journal:onRender() - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante ', - 'nibh_rttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(171) - journal:onRender() - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante ', - 'nibhorttitor mi, vitae rutrum eros metus nec libero._0: Lorem ', - 'ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - for i=1,59 do - simulate_input_keys('CUSTOM_DELETE') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante ', - 'nibhorttitor mi, vitae rutrum eros metus nec libero._', - }, '\n')); - - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante ', - 'nibhorttitor mi, vitae rutrum eros metus nec libero._', - }, '\n')); - - journal:dismiss() -end - -function test.line_end() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('CUSTOM_END') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(70) - journal:onRender() - - simulate_input_keys('CUSTOM_END') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(200) - journal:onRender() - - simulate_input_keys('CUSTOM_END') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_input_keys('CUSTOM_END') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - journal:dismiss() -end - -function test.line_beging() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_HOME') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(173) - journal:onRender() - - simulate_input_keys('CUSTOM_HOME') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '_12: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('CUSTOM_HOME') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.line_delete() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - text_area:setCursor(65) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '_' - }, '\n')); - - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_' - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_' - }, '\n')); - - journal:dismiss() -end - -function test.line_delete_to_end() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - text_area:setCursor(70) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_K') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed_', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_K') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - }, '\n')); - - journal:dismiss() -end - -function test.delete_last_word() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing _', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur _', - }, '\n')); - - text_area:setCursor(82) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed _ urna sit amet aliquet egestas, ante nibh porttitor ', - 'mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur ', - }, '\n')); - - text_area:setCursor(37) - journal:onRender() - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, _ctetur adipiscing elit.', - '112: Sed , urna sit amet aliquet egestas, ante nibh porttitor ', - 'mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur ', - }, '\n')); - - for i=1,6 do - simulate_input_keys('CUSTOM_CTRL_W') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '_ctetur adipiscing elit.', - '112: Sed , urna sit amet aliquet egestas, ante nibh porttitor ', - 'mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur ', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_ctetur adipiscing elit.', - '112: Sed , urna sit amet aliquet egestas, ante nibh porttitor ', - 'mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur ', - }, '\n')); - - journal:dismiss() -end - -function test.jump_to_text_end() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('KEYBOARD_CURSOR_DOWN_FAST') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_DOWN_FAST') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - journal:dismiss() -end - -function test.jump_to_text_begin() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('KEYBOARD_CURSOR_UP_FAST') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP_FAST') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.select_all() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.text_key_replace_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 9, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem '); - - simulate_input_text('+') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: +_psum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 6, 1, 6, 2) - - simulate_input_text('!') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: +ipsum dolor sit amet, consectetur adipiscing elit.', - '112: S!_r mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 3, 1, 6, 2) - - simulate_input_text('@') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: +ipsum dolor sit amet, consectetur adipiscing elit.', - '112@_m ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - journal:dismiss() -end - -function test.arrows_reset_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_A') - - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_A') - - simulate_input_keys('KEYBOARD_CURSOR_UP') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_A') - - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - expect.eq(read_selected_text(text_area), '') - - journal:dismiss() -end - -function test.click_reset_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_mouse_click(text_area, 4, 0) - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_A') - - simulate_mouse_click(text_area, 4, 8) - expect.eq(read_selected_text(text_area), '') - - journal:dismiss() -end - -function test.line_navigation_reset_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_input_keys('CUSTOM_HOME') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_END') - expect.eq(read_selected_text(text_area), '') - - journal:dismiss() -end - -function test.jump_begin_or_end_reset_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_input_keys('KEYBOARD_CURSOR_UP_FAST') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('KEYBOARD_CURSOR_DOWN_FAST') - expect.eq(read_selected_text(text_area), '') - - journal:dismiss() -end - -function test.new_line_override_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 29, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum ero', - }, '\n')); - - simulate_input_keys('SELECT') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ', - '_ metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.backspace_delete_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 29, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum ero', - }, '\n')); - - simulate_input_keys('STRING_A000') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: _ metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.delete_char_delete_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 29, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum ero', - }, '\n')); - - simulate_input_keys('CUSTOM_DELETE') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: _ metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.delete_line_delete_selection_lines() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 9, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem '); - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_12: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 4, 1, 29, 2) - - simulate_input_keys('CUSTOM_CTRL_U') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_1: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - journal:dismiss() -end - -function test.delete_line_rest_delete_selection_lines() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 9, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem '); - - simulate_input_keys('CUSTOM_CTRL_K') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: _', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 6, 1, 6, 2) - - simulate_input_keys('CUSTOM_CTRL_K') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ', - '112: S_', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 3, 1, 6, 2) - - simulate_input_keys('CUSTOM_CTRL_K') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ', - '112_', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - journal:dismiss() -end - -function test.delete_last_word_delete_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 9, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem '); - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: _psum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 6, 1, 6, 2) - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ipsum dolor sit amet, consectetur adipiscing elit.', - '112: S_r mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - simulate_mouse_drag(text_area, 3, 1, 6, 2) - - simulate_input_keys('CUSTOM_CTRL_W') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ipsum dolor sit amet, consectetur adipiscing elit.', - '112_m ipsum dolor sit amet, consectetur adipiscing elit.', - '51: Sed consectetur, urna sit amet aliquet egestas.', - }, '\n')); - - journal:dismiss() -end - -function test.single_mouse_click_set_cursor() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_click(text_area, 4, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: _orem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 40, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus ne_ libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 49, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 60, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero._', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 0, 10) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 21, 10) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor_sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 63, 10) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - journal:dismiss() -end - -function test.double_mouse_click_select_word() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_mouse_click(text_area, 0, 0) - simulate_mouse_click(text_area, 0, 0) - - expect.eq(read_selected_text(text_area), '60:') - - simulate_mouse_click(text_area, 4, 0) - simulate_mouse_click(text_area, 4, 0) - - expect.eq(read_selected_text(text_area), 'Lorem') - - simulate_mouse_click(text_area, 40, 2) - simulate_mouse_click(text_area, 40, 2) - - expect.eq(read_selected_text(text_area), 'nec') - - simulate_mouse_click(text_area, 58, 3) - simulate_mouse_click(text_area, 58, 3) - expect.eq(read_selected_text(text_area), 'elit') - - simulate_mouse_click(text_area, 60, 3) - simulate_mouse_click(text_area, 60, 3) - expect.eq(read_selected_text(text_area), '.') - - journal:dismiss() -end - -function test.double_mouse_click_select_white_spaces() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = 'Lorem ipsum dolor sit amet, consectetur elit.' - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), text .. '_') - - simulate_mouse_click(text_area, 29, 0) - simulate_mouse_click(text_area, 29, 0) - - expect.eq(read_selected_text(text_area), ' ') - - journal:dismiss() -end - -function test.triple_mouse_click_select_line() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_mouse_click(text_area, 0, 0) - simulate_mouse_click(text_area, 0, 0) - simulate_mouse_click(text_area, 0, 0) - - expect.eq( - read_selected_text(text_area), - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - ) - - simulate_mouse_click(text_area, 4, 0) - simulate_mouse_click(text_area, 4, 0) - simulate_mouse_click(text_area, 4, 0) - - expect.eq( - read_selected_text(text_area), - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - ) - - simulate_mouse_click(text_area, 40, 2) - simulate_mouse_click(text_area, 40, 2) - simulate_mouse_click(text_area, 40, 2) - - expect.eq(read_selected_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_mouse_click(text_area, 58, 3) - simulate_mouse_click(text_area, 58, 3) - simulate_mouse_click(text_area, 58, 3) - - expect.eq( - read_selected_text(text_area), - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - ) - - simulate_mouse_click(text_area, 60, 3) - simulate_mouse_click(text_area, 60, 3) - simulate_mouse_click(text_area, 60, 3) - - expect.eq( - read_selected_text(text_area), - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - ) - - journal:dismiss() -end - -function test.mouse_selection_control() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 29, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem ipsum dolor sit amet') - - simulate_mouse_drag(text_area, 0, 0, 29, 0) - - expect.eq(read_selected_text(text_area), '60: Lorem ipsum dolor sit amet') - - simulate_mouse_drag(text_area, 32, 0, 32, 1) - - expect.eq(read_selected_text(text_area), table.concat({ - 'consectetur adipiscing elit.', - '112: Sed consectetur, urna sit am' - }, '\n')); - - simulate_mouse_drag(text_area, 32, 1, 48, 2) - - expect.eq(read_selected_text(text_area), table.concat({ - 'met aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_mouse_drag(text_area, 42, 2, 59, 3) - - expect.eq(read_selected_text(text_area), table.concat({ - 'libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - }, '\n')); - - simulate_mouse_drag(text_area, 42, 2, 65, 3) - - expect.eq(read_selected_text(text_area), table.concat({ - 'libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - }, '\n')); - - simulate_mouse_drag(text_area, 42, 2, 65, 6) - - expect.eq(read_selected_text(text_area), table.concat({ - 'libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.' - }, '\n')); - - simulate_mouse_drag(text_area, 42, 2, 42, 6) - - expect.eq(read_selected_text(text_area), table.concat({ - 'libero.', - '60: Lorem ipsum dolor sit amet, consectetur' - }, '\n')); - - journal:dismiss() -end - -function test.copy_and_paste_text_line() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_C') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_mouse_click(text_area, 15, 3) - simulate_input_keys('CUSTOM_CTRL_C') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum_dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 5, 0) - simulate_input_keys('CUSTOM_CTRL_C') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '112: _ed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 6, 0) - simulate_input_keys('CUSTOM_CTRL_C') - simulate_mouse_click(text_area, 5, 6) - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: L_rem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - journal:dismiss() -end - -function test.copy_and_paste_selected_text() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 8, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem') - - simulate_input_keys('CUSTOM_CTRL_C') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem_ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 4, 2) - - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLorem_itor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 0, 0) - - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Lorem_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLoremtitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 60, 4) - - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Lorem60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLoremtitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.Lorem_', - }, '\n')); - - journal:dismiss() -end - -function test.cut_and_paste_text_line() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit._', - }, '\n')); - - simulate_input_keys('CUSTOM_CTRL_X') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '_', - }, '\n')); - - simulate_mouse_click(text_area, 0, 0) - simulate_input_keys('CUSTOM_CTRL_X') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_click(text_area, 60, 2) - simulate_input_keys('CUSTOM_CTRL_X') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '_', - }, '\n')); - - journal:dismiss() -end - -function test.cut_and_paste_selected_text() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n') - - simulate_input_text(text) - - simulate_mouse_drag(text_area, 4, 0, 8, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - expect.eq(read_selected_text(text_area), 'Lorem') - - simulate_input_keys('CUSTOM_CTRL_X') - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem_ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_drag(text_area, 4, 0, 8, 0) - simulate_input_keys('CUSTOM_CTRL_X') - - simulate_mouse_click(text_area, 4, 2) - - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLorem_itor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_drag(text_area, 5, 2, 8, 2) - simulate_input_keys('CUSTOM_CTRL_X') - - simulate_mouse_click(text_area, 0, 0) - simulate_input_keys('CUSTOM_CTRL_V') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'orem_0: ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLtitor mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - }, '\n')); - - simulate_mouse_drag(text_area, 5, 2, 8, 2) - simulate_input_keys('CUSTOM_CTRL_X') - - simulate_mouse_click(text_area, 60, 4) - simulate_input_keys('CUSTOM_CTRL_V') + local journal, text_area = arrange_empty_journal() + text_area:setText(' ') + journal:onRender() - expect.eq(read_rendered_text(text_area), table.concat({ - 'orem60: ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'portLr mi, vitae rutrum eros metus nec libero.', - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.tito_', - }, '\n')); + expect.eq('dfhack/lua/journal', dfhack.gui.getCurFocus(true)[1]) + expect.eq(read_rendered_text(text_area), '_') journal:dismiss() end @@ -2266,219 +235,6 @@ function test.restore_text_between_sessions() journal:dismiss() end -function test.scroll_long_text() - local journal, text_area = arrange_empty_journal({w=100, h=10}) - local scrollbar = journal.subviews.scrollbar - - local text = table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'Nulla ut lacus ut tortor semper consectetur.', - 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex._', - }, '\n')) - - simulate_mouse_click(scrollbar, 0, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - }, '\n')) - - simulate_mouse_click(scrollbar, 0, 0) - simulate_mouse_click(scrollbar, 0, 0) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - }, '\n')) - - simulate_mouse_click(scrollbar, 0, scrollbar.frame_body.height - 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex._', - }, '\n')) - - simulate_mouse_click(scrollbar, 0, 2) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - }, '\n')) - - journal:dismiss() -end - -function test.scroll_follows_cursor() - local journal, text_area = arrange_empty_journal({w=100, h=10}) - local scrollbar = journal.subviews.text_area_scrollbar - - local text = table.concat({ - 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - 'Nulla ut lacus ut tortor semper consectetur.', - 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex.', - }, '\n') - - simulate_input_text(text) - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - '18: Vestibulum at ante ut dui hendrerit pellentesque ut eu ex._', - }, '\n')) - - simulate_mouse_click(text_area, 0, 8) - simulate_input_keys('KEYBOARD_CURSOR_UP') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_nteger tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - 'Aenean non orci id erat malesuada pharetra.', - 'Nunc in lectus et metus finibus venenatis.', - 'Morbi id mauris dignissim, suscipit metus nec, auctor odio.', - 'Sed in libero eget velit condimentum lacinia ut quis dui.', - 'Praesent sollicitudin dui ac mollis lacinia.', - 'Ut gravida tortor ac accumsan suscipit.', - }, '\n')) - - simulate_input_keys('KEYBOARD_CURSOR_UP_FAST') - - simulate_mouse_click(text_area, 0, 9) - simulate_input_keys('KEYBOARD_CURSOR_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Nulla ut lacus ut tortor semper consectetur.', - 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - '_onec quis lectus ac erat placerat eleifend.', - }, '\n')) - - simulate_mouse_click(text_area, 44, 10) - simulate_input_keys('KEYBOARD_CURSOR_RIGHT') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - '_enean non orci id erat malesuada pharetra.', - }, '\n')) - - simulate_mouse_click(text_area, 0, 2) - simulate_input_keys('KEYBOARD_CURSOR_LEFT') - - expect.eq(read_rendered_text(text_area), table.concat({ - 'Nulla ut lacus ut tortor semper consectetur._', - 'Nam scelerisque ligula vitae magna varius, vel porttitor tellus egestas.', - 'Suspendisse aliquet dolor ac velit maximus, ut tempor lorem tincidunt.', - 'Ut eu orci non nibh hendrerit posuere.', - 'Sed euismod odio eu fringilla bibendum.', - 'Etiam dignissim diam nec aliquet facilisis.', - 'Integer tristique purus at tellus luctus, vel aliquet sapien sollicitudin.', - 'Fusce ornare est vitae urna feugiat, vel interdum quam vestibulum.', - '10: Vivamus id felis scelerisque, lobortis diam ut, mollis nisi.', - 'Donec quis lectus ac erat placerat eleifend.', - }, '\n')) - - journal:dismiss() -end - function test.generate_table_of_contents() local journal, text_area = arrange_empty_journal({w=100, h=10}) @@ -2774,11 +530,6 @@ function test.table_of_contents_selection_follows_cursor() journal:dismiss() end -if df_major_version < 51 then - -- temporary ignore test features that base on newest API of the DF game - return -end - function test.table_of_contents_keyboard_navigation() local journal, text_area = arrange_empty_journal({ w=100, @@ -2849,207 +600,6 @@ function test.table_of_contents_keyboard_navigation() journal:dismiss() end -function test.fast_rewind_words_right() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - text_area:setCursor(1) - journal:onRender() - - simulate_input_keys('A_MOVE_E_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60:_Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_E_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem_ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,6 do - simulate_input_keys('A_MOVE_E_DOWN') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing_', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_E_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit._', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_E_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112:_Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,17 do - simulate_input_keys('A_MOVE_E_DOWN') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - }, '\n')); - - simulate_input_keys('A_MOVE_E_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero._', - }, '\n')); - - journal:dismiss() -end - -function test.fast_rewind_words_left() - local journal, text_area = arrange_empty_journal({w=55}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('A_MOVE_W_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - '_ibero.', - }, '\n')); - - simulate_input_keys('A_MOVE_W_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus _ec ', - 'libero.', - }, '\n')); - - for i=1,8 do - simulate_input_keys('A_MOVE_W_DOWN') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - '_nte nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_W_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet _gestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - for i=1,16 do - simulate_input_keys('A_MOVE_W_DOWN') - end - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_W_DOWN') - - expect.eq(read_rendered_text(text_area), table.concat({ - '_0: Lorem ipsum dolor sit amet, consectetur adipiscing ', - 'elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ', - 'ante nibh porttitor mi, vitae rutrum eros metus nec ', - 'libero.', - }, '\n')); - - journal:dismiss() -end - -function test.fast_rewind_reset_selection() - local journal, text_area = arrange_empty_journal({w=65}) - - local text = table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n') - - simulate_input_text(text) - - simulate_input_keys('CUSTOM_CTRL_A') - - expect.eq(read_rendered_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - expect.eq(read_selected_text(text_area), table.concat({ - '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', - '112: Sed consectetur, urna sit amet aliquet egestas, ante nibh ', - 'porttitor mi, vitae rutrum eros metus nec libero.', - }, '\n')); - - simulate_input_keys('A_MOVE_W_DOWN') - expect.eq(read_selected_text(text_area), '') - - simulate_input_keys('CUSTOM_CTRL_A') - - simulate_input_keys('A_MOVE_E_DOWN') - expect.eq(read_selected_text(text_area), '') - - journal:dismiss() -end - function test.show_tutorials_on_first_use() local journal, text_area, journal_window = arrange_empty_journal({w=65}) simulate_input_keys('CUSTOM_CTRL_O') @@ -3068,6 +618,3 @@ function test.show_tutorials_on_first_use() expect.str_find('Section 1\n', read_rendered_text(toc_panel)); journal:dismiss() end - --- TODO: separate journal tests from TextEditor tests --- add "one_line_mode" tests From cf92110e18033729aab09e0440d8acf09732a911 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 7 Feb 2025 06:26:15 -0600 Subject: [PATCH 405/811] Add devel/ to export-map string --- changelog.txt | 2 +- docs/devel/export-map.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5efe434097..506630f3f3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,7 +28,7 @@ Template for new versions: ## New Tools - `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed. -- `export-map`: Export map tile data to a JSON file. +- `devel/export-map`: Export map tile data to a JSON file. - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk ## New Features diff --git a/docs/devel/export-map.rst b/docs/devel/export-map.rst index 742d9df60f..9301c1d3aa 100644 --- a/docs/devel/export-map.rst +++ b/docs/devel/export-map.rst @@ -1,4 +1,4 @@ -export-map +devel/export-map ========== .. dfhack-tool:: From b6e752bee09fc63438ec58629405167e5131b59e Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 7 Feb 2025 09:50:09 -0600 Subject: [PATCH 406/811] Change examples to use devel/export-map --- docs/devel/export-map.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/devel/export-map.rst b/docs/devel/export-map.rst index 9301c1d3aa..d2b7fca54d 100644 --- a/docs/devel/export-map.rst +++ b/docs/devel/export-map.rst @@ -18,20 +18,20 @@ Usage :: - export-map - export-map (include|exclude) + devel/export-map + devel/export-map (include|exclude) Examples -------- -``export-map`` +``devel/export-map`` Export the map to JSON with ALL data included. -``export-map include -m -s -v`` +``devel/export-map include -m -s -v`` Export the map to JSON with only materials, shape, and variant data included. -``export-map exclude --variant --hidden --light`` +``devel/export-map exclude --variant --hidden --light`` Export the map to JSON with variant, hidden, and light data excluded. From b286e8e700a56c3a697e03665d283874e153d9be Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 7 Feb 2025 10:16:15 -0600 Subject: [PATCH 407/811] Remove wildlife from changelog Co-authored-by: Myk --- changelog.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 506630f3f3..11737f8475 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,7 +27,6 @@ Template for new versions: # Future ## New Tools -- `fix/wildlife`: prevent wildlife from getting stuck when trying to exit the map. This fix needs to be enabled manually in `gui/control-panel` on the Bug Fixes tab since not all players want this bug to be fixed. - `devel/export-map`: Export map tile data to a JSON file. - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk From 3b0999c1f43bc2dea3d3cb2f7cb5ec5974c18740 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 7 Feb 2025 10:17:11 -0600 Subject: [PATCH 408/811] Fix markdown Co-authored-by: Myk --- docs/devel/export-map.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/devel/export-map.rst b/docs/devel/export-map.rst index d2b7fca54d..6beea7378b 100644 --- a/docs/devel/export-map.rst +++ b/docs/devel/export-map.rst @@ -1,5 +1,5 @@ devel/export-map -========== +================ .. dfhack-tool:: :summary: Export map tile data to a JSON file. From eac50825c0f59f457e725d8c17a528f882a67759 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 7 Feb 2025 10:18:08 -0600 Subject: [PATCH 409/811] Remove deprecated module code --- devel/export-map.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/devel/export-map.lua b/devel/export-map.lua index b690938d08..ca45259795 100644 --- a/devel/export-map.lua +++ b/devel/export-map.lua @@ -241,10 +241,6 @@ local function export_fortress_map(options) export_all_z_levels(fortress_name, export_path, options) end -if dfhack_flags.module then - return -end - if not dfhack.isMapLoaded() then qerror('This script requires a map to be loaded') end From 019527f8ee79cd1d5b71b1c91d40d0252aba3ec1 Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 7 Feb 2025 10:22:58 -0600 Subject: [PATCH 410/811] Add more detailed map size explination --- devel/export-map.lua | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/devel/export-map.lua b/devel/export-map.lua index ca45259795..b4725ccb88 100644 --- a/devel/export-map.lua +++ b/devel/export-map.lua @@ -43,8 +43,10 @@ local function get_evilness() end local function classify_tile(options, x, y, z) - -- The last z-levels of hell shrink their x/y size unexpectedly! (ಠ_ಠ) - -- if your map is 190x190, the last hell z-levels are gonna be like 90x90 + -- if your map happens to cross a region boundary and different regions are + -- different depths, the last z-levels of hell MIGHT shrink their x/y size + -- so if your map is 190x190, the last hell z-levels can end up being 90x90 + if dfhack.maps.getTileType(x, y, z) == nil then return nil -- Designating the non-tiles of hell to be nil end From 4745efb4d3b010374775eca4dbd6bd449c3190ac Mon Sep 17 00:00:00 2001 From: Tim Date: Fri, 7 Feb 2025 12:48:54 -0600 Subject: [PATCH 411/811] Fix evilness and underworld options not working with include and exclude commands --- devel/export-map.lua | 24 ++++++++++++++---------- 1 file changed, 14 insertions(+), 10 deletions(-) diff --git a/devel/export-map.lua b/devel/export-map.lua index b4725ccb88..2fc9efd523 100644 --- a/devel/export-map.lua +++ b/devel/export-map.lua @@ -8,8 +8,8 @@ local utils = require('utils') local json = require('json') local argparse = require('argparse') -local include_underworld_z = false local underworld_z +local underworld local evilness -- the layer of the underworld @@ -200,16 +200,16 @@ local function export_all_z_levels(fortress_name, folder, options) x = xmax, y = ymax, -- subtract underworld levels if excluded from options - z = include_underworld_z and zmax or (zmax - underworld_z), - underworld_z_level = include_underworld_z and underworld_z or nil, - evilness = evilness or nil, + z = underworld and zmax or (zmax - underworld_z), + underworld_z_level = underworld and underworld_z or nil, + evilness = evilness and get_evilness() or nil, } data.KEYS = setup_keys(options) data.map = {} local zmin = 0 - if not include_underworld_z then -- skips all z-levels in the underworld + if not underworld then -- skips all z-levels in the underworld zmin = underworld_z end @@ -261,6 +261,8 @@ local options, args = { material = false, flow = false, liquid = false, + underworld = false, + evilness = false, }, {...} local positionals = argparse.processArgsGetopt(args, { @@ -277,9 +279,8 @@ local positionals = argparse.processArgsGetopt(args, { {'m', 'material', handler=function() options.material = true end}, {'f', 'flow', handler=function() options.flow = true end}, {'q', 'liquid', handler=function() options.liquid = true end}, - -- local var since underworld not in ordered option - {'u', 'underworld', handler=function() include_underworld_z = true end}, - {'e', 'evilness', handler=function() evilness = get_evilness() end}, + {'u', 'underworld', handler=function() options.underworld = true end}, + {'e', 'evilness', handler=function() options.evilness = true end}, }) if positionals[1] == "help" or options.help then @@ -295,8 +296,6 @@ else -- include everything for setting in pairs(options) do options[setting] = true end - -- don't forget to include underworld - include_underworld_z = true end local ordered_options = { @@ -314,6 +313,11 @@ local ordered_options = { "liquid", } +-- these get omitted from ordered_options since this data goes directly into the +-- JSON object for MAP_SIZE and doesn't need to be parsed into every tile +underworld = options.underworld +evilness = options.evilness + -- reorganize ordered options based on selected options via argparse -- this is so ARGUMENT_OPTION_ORDER has the correct order with no gaps for setting in pairs(options) do From 3f42783b7da04d2e3cd5066078c6714080902f19 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 23:09:25 -0800 Subject: [PATCH 412/811] clean up scan-vtables and ensure scan order --- devel/scan-vtables.lua | 93 +++++++++++++++++++++++++++--------------- 1 file changed, 59 insertions(+), 34 deletions(-) diff --git a/devel/scan-vtables.lua b/devel/scan-vtables.lua index 1b62ec424b..069e43c0b0 100644 --- a/devel/scan-vtables.lua +++ b/devel/scan-vtables.lua @@ -1,48 +1,74 @@ -- Scan and dump likely vtable addresses -memscan = require('memscan') +local memscan = require('memscan') local osType = dfhack.getOSType() if osType ~= 'linux' then qerror('unsupported OS: ' .. osType) end -local df_ranges = {} -for _, mem in ipairs(dfhack.internal.getMemRanges()) do - if mem.read and ( - string.match(mem.name,'/dwarfort%.exe$') - or string.match(mem.name,'/dwarfort$') - or string.match(mem.name,'/Dwarf_Fortress$') - or string.match(mem.name,'Dwarf Fortress%.exe') - or string.match(mem.name,'/libg_src_lib.so$') - ) - then - table.insert(df_ranges, mem) +local function get_ranges() + local df_ranges, lib_names = {}, {} + + local raw_ranges = dfhack.internal.getMemRanges() + + -- add main binary mem ranges first + for _, range in ipairs(raw_ranges) do + if range.read and ( + string.match(range.name, '/dwarfort$') or + string.match(range.name, 'Dwarf Fortress%.exe') + ) + then + table.insert(df_ranges, range) + end + end + + for _, range in ipairs(raw_ranges) do + if range.read and string.match(range.name, '/libg_src_lib.so$') then + table.insert(df_ranges, range) + lib_names[range.name] = true + end + end + + return df_ranges, lib_names +end + +local df_ranges, lib_names = get_ranges() + +-- vtables that cross a range boundary can appear twice, a truncated version in the +-- lower memory range and a full version in the higher memory range +-- therefore, sort the memory ranges by start address, descending +-- but keep libraries last +local function sort_ranges(a, b) + if lib_names[a.name] == lib_names[b.name] then + return a.start_addr > b.start_addr end + return lib_names[b.name] end +table.sort(df_ranges, sort_ranges) + function is_df_addr(a) - for _, mem in ipairs(df_ranges) do - if a >= mem.start_addr and a < mem.end_addr then + for _, range in ipairs(df_ranges) do + if a >= range.start_addr and a < range.end_addr then return true end end return false end -local names = {} +local function is_vtable_range(range) + return not range.write and not range.execute +end -function scan_ranges(g_src) +function scan_ranges() local vtables = {} - for _, range in ipairs(df_ranges) do - if (not range.read) or range.write or range.execute then - goto next_range - end - if not not range.name:match('g_src') ~= g_src then - goto next_range - end + local seen = {} -- only record the first encountered vtable for each name + for _, range in ipairs(df_ranges) do + if not is_vtable_range(range) then goto next_range end local base = range.name:match('.*/(.*)$') local area = memscan.MemoryArea.new(range.start_addr, range.end_addr) + local is_lib = lib_names[range.name] for i = 1, area.uintptr_t.count - 1 do -- take every pointer-aligned value in memory mapped to the DF executable, and see if it is a valid vtable -- start by following the logic in Process::doReadClassName() and ensure it doesn't crash @@ -71,27 +97,26 @@ function scan_ranges(g_src) if demangled_name and not demangled_name:match('[<>]') and not demangled_name:match('^std::') and - not names[demangled_name] and - (g_src or demangled_name ~= 'widgets::widget') -- the widget in g_src takes precedence + not seen[demangled_name] and + (is_lib or demangled_name ~= 'widgets::widget') -- the widget in g_src takes precedence then local base_str = '' - if g_src then + if is_lib then vtable = vtable - range.base_addr base_str = (" base='%s'"):format(base) end vtables[demangled_name] = {value=vtable, base_str=base_str} + seen[demangled_name] = true end ::next_ptr:: end ::next_range:: end - for name, data in pairs(vtables) do - if not names[name] then - print((""):format(name, data.value, data.base_str)) - names[name] = true - end - end + + return vtables end -scan_ranges(false) -scan_ranges(true) +local vtables = scan_ranges() +for name, data in pairs(vtables) do + print((""):format(name, data.value, data.base_str)) +end From c26e0a9def10baa0cbdbee3f8b93275f2ef88730 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 00:07:51 -0800 Subject: [PATCH 413/811] convert the keys of the allowed table to strings for persistence so we don't end up with huge null arrays in the json --- idle-crafting.lua | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/idle-crafting.lua b/idle-crafting.lua index f78e667631..39a647731c 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -235,11 +235,34 @@ watched = watched or {} ---@type integer[] thresholds = thresholds or { 10000, 1000, 500 } +-- persisting a table with numeric keys results in a json array with a huge number of null entries +-- therefore, we convert the keys to strings for persistence +-- also, we clear the frame counter values since the frame counter gets reset on load +local function to_persist_allowed() + local persistable_allowed = {} + for workshop_id in pairs(allowed) do + persistable_allowed[tostring(workshop_id)] = -1 + end + return persistable_allowed +end + +-- loads both from the older array format and the new string table format +local function from_persist_allowed(persisted_allowed) + if not persisted_allowed then + return + end + local usable_allowed = {} + for workshop_id in pairs(persisted_allowed) do + usable_allowed[tonumber(workshop_id)] = -1 + end + return usable_allowed +end + local function persist_state() dfhack.persistent.saveSiteData(GLOBAL_KEY, { - enabled = enabled, - allowed = allowed, - thresholds = thresholds + enabled=enabled, + allowed=to_persist_allowed(), + thresholds=thresholds }) end @@ -248,7 +271,7 @@ local function load_state() -- load persistent data local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) enabled = persisted_data.enabled or false - allowed = persisted_data.allowed or {} + allowed = from_persist_allowed(persisted_data.allowed) or {} thresholds = persisted_data.thresholds or { 10000, 1000, 500 } end @@ -392,7 +415,6 @@ local function processUnit(workshop, idx, unit_id) end end if success then - -- Why is the encoding still wrong, even when using df2console? print('idle-crafting: assigned crafting job to ' .. dfhack.df2console(dfhack.units.getReadableName(unit))) watched[idx][unit_id] = nil allowed[workshop.id] = df.global.world.frame_counter From 2c7368cff9cdc2127c9cd55e65a7503bb33881cb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 14:53:14 -0800 Subject: [PATCH 414/811] update scripts for great reorg --- assign-preferences.lua | 20 +++---- autocheese.lua | 2 +- deep-embark.lua | 2 +- devel/export-dt-ini.lua | 32 +++++------ devel/sc.lua | 1 - diplomacy.lua | 8 +-- docs/modtools/pref-edit.rst | 2 +- exportlegends.lua | 14 ++--- fix/civil-war.lua | 2 +- gui/advfort.lua | 14 ++--- gui/civ-alert.lua | 2 +- gui/notify.lua | 2 +- gui/sandbox.lua | 2 +- immortal-cravings.lua | 4 +- internal/advfort/advfort_items.lua | 2 +- internal/advtools/fastcombat.lua | 4 +- internal/caravan/common.lua | 4 +- internal/confirm/specs.lua | 2 +- internal/exportlegends/racefilter.lua | 2 +- internal/notify/notifications.lua | 2 +- internal/quickfort/place.lua | 43 +++++++------- internal/quickfort/zone.lua | 8 +-- lever.lua | 25 +++++---- load-save.lua | 2 +- makeown.lua | 2 +- modtools/create-unit.lua | 4 +- modtools/equip-item.lua | 2 +- modtools/item-trigger.lua | 4 +- modtools/moddable-gods.lua | 2 +- modtools/pref-edit.lua | 4 +- pref-adjust.lua | 80 +++++++++++++-------------- remove-stress.lua | 4 +- stripcaged.lua | 4 +- uniform-unstick.lua | 6 +- workorder.lua | 16 +++--- 35 files changed, 165 insertions(+), 164 deletions(-) diff --git a/assign-preferences.lua b/assign-preferences.lua index dae521d379..86cc3fbf2f 100644 --- a/assign-preferences.lua +++ b/assign-preferences.lua @@ -29,7 +29,7 @@ end local function format_preference(pref, index) print(string.format("Preference #%d:", index)) - local pref_type = df.unit_preference.T_type[pref.type] + local pref_type = df.unitpref_type[pref.type] local description = "" if pref_type == "LikeMaterial" then description = "Likes material: " .. dfhack.matinfo.getToken(pref.mattype, pref.matindex) @@ -76,7 +76,7 @@ local preference_functions = { local ret = {} if mat_info then ret = { --luacheck:retype - type = df.unit_preference.T_type.LikeMaterial, + type = df.unitpref_type.LikeMaterial, item_type = -1, creature_id = -1, color_id = -1, @@ -109,7 +109,7 @@ local preference_functions = { local index = utils.linear_index(df.global.world.raws.creatures.all, creature_id, "creature_id") if index then return { - type = df.unit_preference.T_type.LikeCreature, + type = df.unitpref_type.LikeCreature, item_type = index, creature_id = index, color_id = index, @@ -186,7 +186,7 @@ local preference_functions = { if item_type then return { - type = df.unit_preference.T_type.LikeFood, + type = df.unitpref_type.LikeFood, item_type = item_type, creature_id = item_type, color_id = item_type, @@ -221,7 +221,7 @@ local preference_functions = { local index = utils.linear_index(df.global.world.raws.creatures.all, creature_id, "creature_id") if index then return { - type = df.unit_preference.T_type.HateCreature, + type = df.unitpref_type.HateCreature, item_type = index, creature_id = index, color_id = index, @@ -262,7 +262,7 @@ local preference_functions = { do if item_type then return { - type = df.unit_preference.T_type.LikeItem, + type = df.unitpref_type.LikeItem, item_type = item_type, creature_id = item_type, color_id = item_type, @@ -297,7 +297,7 @@ local preference_functions = { local index = utils.linear_index(df.global.world.raws.plants.all, plant_id, "id") if index then return { - type = df.unit_preference.T_type.LikePlant, + type = df.unitpref_type.LikePlant, item_type = index, creature_id = index, color_id = index, @@ -330,7 +330,7 @@ local preference_functions = { local index = utils.linear_index(df.global.world.raws.plants.all, plant_id, "id") if index then return { - type = df.unit_preference.T_type.LikeTree, + type = df.unitpref_type.LikeTree, item_type = index, creature_id = index, color_id = index, @@ -364,7 +364,7 @@ local preference_functions = { local _, found, index = utils.binsearch(df.global.world.raws.descriptors.colors, color_name, "id") if found then return { - type = df.unit_preference.T_type.LikeColor, + type = df.unitpref_type.LikeColor, item_type = index, creature_id = index, color_id = index, @@ -397,7 +397,7 @@ local preference_functions = { local index, _ = utils.linear_index(df.global.world.raws.descriptors.shapes, shape_name, "id") if index then return { - type = df.unit_preference.T_type.LikeShape, + type = df.unitpref_type.LikeShape, item_type = index, creature_id = index, color_id = index, diff --git a/autocheese.lua b/autocheese.lua index b365f3f7b1..c857b3b57e 100644 --- a/autocheese.lua +++ b/autocheese.lua @@ -18,7 +18,7 @@ function makeCheese(barrel, workshop) jitem.flags1.milk = true job.job_items.elements:insert('#', jitem) - if not dfhack.job.attachJobItem(job, barrel, df.job_item_ref.T_role.Reagent, 0, -1) then + if not dfhack.job.attachJobItem(job, barrel, df.jjob_role_type.Reagent, 0, -1) then dfhack.error('could not attach item') end diff --git a/deep-embark.lua b/deep-embark.lua index 1745774a9b..6738bd532f 100644 --- a/deep-embark.lua +++ b/deep-embark.lua @@ -137,7 +137,7 @@ end function moveEmbarkStuff(selectedBlock, embarkTiles) local spawnPosCentre for _, hotkey in ipairs(df.global.plotinfo.main.hotkeys) do - if hotkey.cmd == df.ui_hotkey.T_cmd.Zoom then -- the preset hotkey is centred around the spawn point + if hotkey.cmd == df.hotkey_type.Zoom then -- the preset hotkey is centred around the spawn point spawnPosCentre = xyz2pos(hotkey.x, hotkey.y, hotkey.z) hotkey:assign(embarkTiles[math.random(1, #embarkTiles)]) -- set the hotkey to the new spawn point break diff --git a/devel/export-dt-ini.lua b/devel/export-dt-ini.lua index 5d8169697b..e1ea4439b7 100644 --- a/devel/export-dt-ini.lua +++ b/devel/export-dt-ini.lua @@ -201,7 +201,7 @@ address('hist_name',df.historical_figure,'name') address('id',df.historical_figure,'id') address('hist_fig_info',df.historical_figure,'info') address('reputation',df.historical_figure_info,'reputation') -address('current_ident',df.historical_figure_info.T_reputation,'cur_identity') +address('current_ident',df.reputation_profilest,'cur_identity') address('fake_name',df.identity,'name') address('fake_birth_year',df.identity,'birth_year') address('fake_birth_time',df.identity,'birth_second') @@ -356,16 +356,16 @@ address('trans_race_vec',df.creature_interaction_effect_body_transformationst,'r header('unit_wound_offsets') address('parts',df.unit_wound,'parts') -address('id',df.unit_wound.T_parts,'body_part_id') -address('layer',df.unit_wound.T_parts,'layer_idx') +address('id',df.unit_wound_layerst,'body_part_id') +address('layer',df.unit_wound_layerst,'layer_idx') address('general_flags',df.unit_wound,'flags') -address('flags1',df.unit_wound.T_parts,'flags1') -address('flags2',df.unit_wound.T_parts,'flags2') -address('effects_vector',df.unit_wound.T_parts,'effect_type') -address('bleeding',df.unit_wound.T_parts,'bleeding') -address('pain',df.unit_wound.T_parts,'pain') -address('cur_pen',df.unit_wound.T_parts,'cur_penetration_perc') -address('max_pen',df.unit_wound.T_parts,'max_penetration_perc') +address('flags1',df.unit_wound_layerst,'flags1') +address('flags2',df.unit_wound_layerst,'flags2') +address('effects_vector',df.unit_wound_layerst,'effect_type') +address('bleeding',df.unit_wound_layerst,'bleeding') +address('pain',df.unit_wound_layerst,'pain') +address('cur_pen',df.unit_wound_layerst,'cur_penetration_perc') +address('max_pen',df.unit_wound_layerst,'max_penetration_perc') header('soul_details') address('name',df.unit_soul,'name') @@ -377,7 +377,7 @@ address('personality',df.unit_soul,'personality') address('beliefs',df.unit_personality,'values') address('emotions',df.unit_personality,'emotions') address('goals',df.unit_personality,'dreams') -address('goal_realized',df.unit_personality.T_dreams,'flags') +address('goal_realized',df.personality_goalst,'flags') address('traits',df.unit_personality,'traits') address('stress_level',df.unit_personality,'stress') address('needs',df.unit_personality,'needs') @@ -387,10 +387,10 @@ address('combat_hardened',df.unit_personality,'combat_hardened') address('likes_outdoors',df.unit_personality,'likes_outdoors') header('need_offsets') -address('id',df.unit_personality.T_needs,'id') -address('deity_id',df.unit_personality.T_needs,'deity_id') -address('focus_level',df.unit_personality.T_needs,'focus_level') -address('need_level',df.unit_personality.T_needs,'need_level') +address('id',df.personality_needst,'id') +address('deity_id',df.personality_needst,'deity_id') +address('focus_level',df.personality_needst,'focus_level') +address('need_level',df.personality_needst,'need_level') header('emotion_offsets') address('emotion_type',df.personality_moodst,'type') @@ -456,7 +456,7 @@ address('knowledge_category',df.activity_event_ponder_topicst,'topic','research' address('knowledge_flag',df.activity_event_ponder_topicst,'topic','research','flag_data') address('perf_type',df.activity_event_performancest,'type') address('perf_participants',df.activity_event_performancest,'participant_actions') -address('perf_histfig',df.activity_event_performancest.T_participant_actions,'histfig_id') +address('perf_histfig',df.performance_rolest,'histfig_id') header('art_offsets') address('name',df.poetic_form,'name') diff --git a/devel/sc.lua b/devel/sc.lua index 3c3eb7c6cc..085514fc9b 100644 --- a/devel/sc.lua +++ b/devel/sc.lua @@ -175,7 +175,6 @@ local function check_container(obj, path) if not (obj._type == df.unit_preference and k == 'item_type') and not (obj._type == df.unit.T_job and k == 'mood_skill') and not (obj._type == df.unit and k == 'idle_area_type') - and not (obj._type == df.history_event_body_abusedst.T_props) and not (field._type == df.skill_rating) and field.value >= -1 and field.value < 1024 then local key = tostring(obj._type) .. '.' .. k .. tostring(field.value) diff --git a/diplomacy.lua b/diplomacy.lua index 7b44efa4cf..512d0df4d0 100644 --- a/diplomacy.lua +++ b/diplomacy.lua @@ -7,7 +7,7 @@ local p_civ = df.historical_entity.find(df.global.plotinfo.civ_id) -- get list of civs: function get_civ_list() local civ_list = {} - for _, entity in pairs(p_civ.relations.diplomacy) do + for _, entity in ipairs(p_civ.relations.diplomacy.state) do local cur_civ_id = entity.group_id local cur_civ = df.historical_entity.find(cur_civ_id) if cur_civ.type == 0 then @@ -19,7 +19,7 @@ function get_civ_list() rel_str = "War" end matched = "No" - for _, entity2 in pairs(cur_civ.relations.diplomacy) do + for _, entity2 in ipairs(cur_civ.relations.diplomacy.state) do if entity2.group_id == p_civ_id and entity2.relation == entity.relation then matched = "Yes" end @@ -51,12 +51,12 @@ end -- change relation: function change_relation(civ_id, relation) print("Changing relation with " .. civ_id .. " to " .. (relation == 0 and "Peace" or "War")) - for _, entity in pairs(p_civ.relations.diplomacy) do + for _, entity in ipairs(p_civ.relations.diplomacy.state) do local cur_civ_id = entity.group_id local cur_civ = df.historical_entity.find(cur_civ_id) if cur_civ.type == 0 and cur_civ_id == civ_id then entity.relation = relation - for _, entity2 in pairs(cur_civ.relations.diplomacy) do + for _, entity2 in pairs(cur_civ.relations.diplomacy.state) do if entity2.group_id == p_civ_id then entity2.relation = relation end diff --git a/docs/modtools/pref-edit.rst b/docs/modtools/pref-edit.rst index c305c97a0a..cc154e117c 100644 --- a/docs/modtools/pref-edit.rst +++ b/docs/modtools/pref-edit.rst @@ -34,7 +34,7 @@ Valid filters: Include one of these to describe what the id argument represents. - ``-type ``: This describes the type of the preference. Can be entered either using the numerical ID or text id. - Run ``lua @df.unit_preference.T_type`` for a full list of valid values. + Run ``lua @df.unitpref_type`` for a full list of valid values. - ``-subtype ``: The value for an item's subtype - ``-material ``: diff --git a/exportlegends.lua b/exportlegends.lua index e4c5eedea4..3e65abd68b 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -717,7 +717,7 @@ local function export_more_legends_xml() file:write("\t\t<"..k..">"..escape_xml(dfhack.df2utf(df.creature_raw.find(detailV).name[0])).."\n") end elseif df.history_event_body_abusedst:is_instance(event) and (k == "abuse_data") then - if event.abuse_type == df.history_event_body_abusedst.T_abuse_type.Impaled then + if event.abuse_type == df.body_abuse_method_type.Impaled then file:write("\t\t"..tostring(df_enums.item_type[event.abuse_data.Impaled.item_type]):lower().."\n") file:write("\t\t"..getItemSubTypeName(event.abuse_data.Impaled.item_type,event.abuse_data.Impaled.item_subtype).."\n") if (event.abuse_data.Impaled.mat_type > -1) then @@ -728,16 +728,16 @@ local function export_more_legends_xml() file:write("\t\t"..dfhack.df2utf(dfhack.matinfo.toString(dfhack.matinfo.decode(event.abuse_data.Impaled.mat_type, event.abuse_data.Impaled.mat_index))).."\n") end end - elseif event.abuse_type == df.history_event_body_abusedst.T_abuse_type.Piled then - local val = df.history_event_body_abusedst.T_abuse_data.T_Piled.T_pile_type [event.abuse_data.Piled.pile_type] + elseif event.abuse_type == df.body_abuse_method_type.Piled then + local val = df.body_abuse_sculpture_type [event.abuse_data.Piled.pile_type] if not val then file:write("\t\tunknown "..tostring(event.abuse_data.Piled.pile_type).."\n") else file:write("\t\t"..tostring(val):lower().."\n") end - elseif event.abuse_type == df.history_event_body_abusedst.T_abuse_type.Flayed then + elseif event.abuse_type == df.body_abuse_method_type.Flayed then file:write("\t\t"..tostring(event.abuse_data.Flayed.structure).."\n") - elseif event.abuse_type == df.history_event_body_abusedst.T_abuse_type.Hung then + elseif event.abuse_type == df.body_abuse_method_type.Hung then file:write("\t\t"..tostring(event.abuse_data.Hung.tree).."\n") if (dfhack.matinfo.decode(event.abuse_data.Hung.mat_type, event.abuse_data.Hung.mat_index) == nil) then file:write("\t\t"..event.abuse_data.Hung.mat_type.."\n") @@ -745,8 +745,8 @@ local function export_more_legends_xml() else file:write("\t\t"..dfhack.df2utf(dfhack.matinfo.toString(dfhack.matinfo.decode(event.abuse_data.Hung.mat_type, event.abuse_data.Hung.mat_index))).."\n") end - elseif event.abuse_type == df.history_event_body_abusedst.T_abuse_type.Mutilated then -- For completeness. No fields - elseif event.abuse_type == df.history_event_body_abusedst.T_abuse_type.Animated then + elseif event.abuse_type == df.body_abuse_method_type.Mutilated then -- For completeness. No fields + elseif event.abuse_type == df.body_abuse_method_type.Animated then file:write("\t\t"..tostring(event.abuse_data.Animated.interaction).."\n") end elseif df.history_event_assume_identityst:is_instance(event) and k == "identity" then diff --git a/fix/civil-war.lua b/fix/civil-war.lua index 80086bc1b1..2f05773071 100644 --- a/fix/civil-war.lua +++ b/fix/civil-war.lua @@ -3,7 +3,7 @@ local civ = df.historical_entity.find(df.global.plotinfo.civ_id) local fixed = false -for _, entity in pairs(civ.relations.diplomacy) do +for _, entity in ipairs(civ.relations.diplomacy.state) do if entity.group_id == civ.id and entity.relation > 0 then entity.relation = 0 fixed = true diff --git a/gui/advfort.lua b/gui/advfort.lua index 125bed7ba8..9560b52bca 100644 --- a/gui/advfort.lua +++ b/gui/advfort.lua @@ -837,8 +837,8 @@ end function EnumItems_with_settings( args ) if settings.check_inv then return EnumItems{pos=args.from_pos,unit=args.unit, - inv={[df.unit_inventory_item.T_mode.Hauled]=settings.use_worn,[df.unit_inventory_item.T_mode.Worn]=settings.use_worn, - [df.unit_inventory_item.T_mode.Weapon]=settings.use_worn,},deep=true} + inv={[df.inv_item_role_type.Hauled]=settings.use_worn,[df.inv_item_role_type.Worn]=settings.use_worn, + [df.inv_item_role_type.Weapon]=settings.use_worn,},deep=true} else return EnumItems{pos=args.from_pos} end @@ -872,7 +872,7 @@ function find_suitable_items(job,items,job_items) if not settings.gui_item_select then if (item_counts[job_id]>0 and item_suitable) or settings.build_by_items then --cur_item.flags.in_job=true - job.items:insert("#",{new=true,item=cur_item,role=df.job_item_ref.T_role.Reagent,job_item_idx=job_id}) + job.items:insert("#",{new=true,item=cur_item,role=df.jjob_role_type.Reagent,job_item_idx=job_id}) item_counts[job_id]=item_counts[job_id]-cur_item:getTotalDimension() --print(string.format("item added, job_item_id=%d, item %s, quantity left=%d",job_id,tostring(cur_item),item_counts[job_id])) used_item_id[cur_item.id]=true @@ -1021,8 +1021,8 @@ end -- print("AAA FAILED!") -- return false -- end --- args.job.items[0].role=df.job_item_ref.T_role.LinkToTarget --- args.job.items[1].role=df.job_item_ref.T_role.LinkToTrigger +-- args.job.items[0].role=df.jjob_role_type.LinkToTarget +-- args.job.items[1].role=df.jjob_role_type.LinkToTrigger -- end function fake_linking(lever,building,slots) local item1=slots[1].items[1] @@ -1331,8 +1331,8 @@ end function usetool:openPutWindow(building) local adv=df.global.world.units.active[0] local items=EnumItems{pos=adv.pos,unit=adv, - inv={[df.unit_inventory_item.T_mode.Hauled]=true,--[df.unit_inventory_item.T_mode.Worn]=true, - [df.unit_inventory_item.T_mode.Weapon]=true,},deep=true} + inv={[df.inv_item_role_type.Hauled]=true,--[df.inv_item_role_type.Worn]=true, + [df.inv_item_role_type.Weapon]=true,},deep=true} local choices={} for k,v in pairs(items) do table.insert(choices,{text=dfhack.items.getDescription(v,0),item=v}) diff --git a/gui/civ-alert.lua b/gui/civ-alert.lua index 6e4b0cef79..45efacf4eb 100644 --- a/gui/civ-alert.lua +++ b/gui/civ-alert.lua @@ -9,7 +9,7 @@ local widgets = require('gui.widgets') local function get_civ_alert() local list = df.global.plotinfo.alerts.list while #list < 2 do - local list_item = df.plotinfost.T_alerts.T_list:new() + local list_item = df.alert_statest:new() list_item.id = df.global.plotinfo.alerts.next_id df.global.plotinfo.alerts.next_id = df.global.plotinfo.alerts.next_id + 1 list_item.name = 'civ-alert' diff --git a/gui/notify.lua b/gui/notify.lua index cc469d1480..d9b3eaa163 100644 --- a/gui/notify.lua +++ b/gui/notify.lua @@ -161,7 +161,7 @@ AdvNotifyOverlay.ATTRS{ function AdvNotifyOverlay:set_width() local desired_width = 13 - if df.global.adventure.player_control_state ~= df.adventurest.T_player_control_state.TAKING_INPUT then + if df.global.adventure.player_control_state ~= df.adventure_game_loop_type.TAKING_INPUT then local offset = self.frame_parent_rect.width > 137 and 26 or (self.frame_parent_rect.width+1) // 2 - 43 desired_width = self.frame_parent_rect.width // 2 + offset diff --git a/gui/sandbox.lua b/gui/sandbox.lua index e99e3d3a64..8418c79377 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -448,7 +448,7 @@ local function init_arena() if #list > list_size then utils.assign(list[list_size], element) else - element.new = df.embark_item_choice.T_list + element.new = df.itinfost list:insert('#', element) end list_size = list_size + 1 diff --git a/immortal-cravings.lua b/immortal-cravings.lua index 5ec1519931..a74d5aeb33 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -74,7 +74,7 @@ local function goDrink(unit) job.flags.special = true local dx, dy, dz = dfhack.items.getPosition(drink) job.pos = xyz2pos(dx, dy, dz) - if not dfhack.job.attachJobItem(job, drink, df.job_item_ref.T_role.Other, -1, -1) then + if not dfhack.job.attachJobItem(job, drink, df.jjob_role_type.Other, -1, -1) then error('could not attach drink') return end @@ -96,7 +96,7 @@ local function goEat(unit) job.flags.special = true local dx, dy, dz = dfhack.items.getPosition(meal) job.pos = xyz2pos(dx, dy, dz) - if not dfhack.job.attachJobItem(job, meal, df.job_item_ref.T_role.Other, -1, -1) then + if not dfhack.job.attachJobItem(job, meal, df.jjob_role_type.Other, -1, -1) then error('could not attach meal') return end diff --git a/internal/advfort/advfort_items.lua b/internal/advfort/advfort_items.lua index e89cd8e9f1..4a50fe264b 100644 --- a/internal/advfort/advfort_items.lua +++ b/internal/advfort/advfort_items.lua @@ -189,7 +189,7 @@ function jobitemEditor:commit() for _,slot in pairs(self.slots) do if slot.id == orderedSlotID then for _1,cur_item in pairs(slot.items) do - self.job.items:insert("#",{new=true,item=cur_item,role=df.job_item_ref.T_role.Reagent,job_item_idx=slot.id}) + self.job.items:insert("#",{new=true,item=cur_item,role=df.jjob_role_type.Reagent,job_item_idx=slot.id}) end end end diff --git a/internal/advtools/fastcombat.lua b/internal/advtools/fastcombat.lua index 2b9cd184f0..f9d87c6868 100644 --- a/internal/advtools/fastcombat.lua +++ b/internal/advtools/fastcombat.lua @@ -30,7 +30,7 @@ function AdvCombatOverlay:preUpdateLayout(parent_rect) end function AdvCombatOverlay:render(dc) - if df.global.adventure.player_control_state == df.adventurest.T_player_control_state.TAKING_INPUT then + if df.global.adventure.player_control_state == df.adventure_game_loop_type.TAKING_INPUT then self.skip_combat = false return end @@ -62,7 +62,7 @@ local COMBAT_MOVE_KEYS = { function AdvCombatOverlay:onInput(keys) for code,_ in pairs(keys) do if not COMBAT_MOVE_KEYS[code] then goto continue end - if df.global.adventure.player_control_state ~= df.adventurest.T_player_control_state.TAKING_INPUT then + if df.global.adventure.player_control_state ~= df.adventure_game_loop_type.TAKING_INPUT then -- Instantly speed up the combat self.skip_combat = true elseif df.global.world.status.temp_flag.adv_showing_announcements then diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 061accb5a5..ec53bd585e 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -335,7 +335,7 @@ end function get_banned_items() local banned_items = {} for _, mandate in ipairs(df.global.world.mandates) do - if mandate.mode == df.mandate.T_mode.Export then + if mandate.mode == df.mandate_type.Export then register_item_type(banned_items, mandate) end end @@ -344,7 +344,7 @@ end local function analyze_noble(unit, risky_items, banned_items) for _, preference in ipairs(unit.status.current_soul.preferences) do - if preference.type == df.unit_preference.T_type.LikeItem and + if preference.type == df.unitpref_type.LikeItem and preference.active then register_item_type(risky_items, preference, banned_items) diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 3fb9ed13c4..67628222aa 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -367,7 +367,7 @@ ConfirmSpec{ local max_visible_buttons = num_sections // 2 if selected_offset >= max_visible_buttons or selected_idx >= num_hotkeys or - plotinfo.main.hotkeys[selected_idx].cmd == df.ui_hotkey.T_cmd.None + plotinfo.main.hotkeys[selected_idx].cmd == df.hotkey_type.None then return false end diff --git a/internal/exportlegends/racefilter.lua b/internal/exportlegends/racefilter.lua index 7270396c23..48af2be76c 100644 --- a/internal/exportlegends/racefilter.lua +++ b/internal/exportlegends/racefilter.lua @@ -106,7 +106,7 @@ end local function is_hf_page(scr, page) page = page or get_cur_page(scr) - return page.mode == df.legend_pagest.T_mode.HFS and page.index == -1 + return page.mode == df.legends_mode_type.HFS and page.index == -1 end function RaceFilterOverlay:render(dc) diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index e992feb2e7..b053689bd4 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -384,7 +384,7 @@ NOTIFICATIONS_BY_IDX = { dwarf_fn=function() local count = 0 for _, mandate in ipairs(df.global.world.mandates) do - if mandate.mode == df.mandate.T_mode.Make and + if mandate.mode == df.mandate_type.Make and mandate.timeout_limit - mandate.timeout_counter < 2500 then count = count + 1 diff --git a/internal/quickfort/place.lua b/internal/quickfort/place.lua index c3533103f4..e9bc42a179 100644 --- a/internal/quickfort/place.lua +++ b/internal/quickfort/place.lua @@ -155,7 +155,7 @@ local function make_db_entry(keys) num_barrels=num_barrels, num_wheelbarrows=num_wheelbarrows, links={give_to={}, take_from={}}, - props={}, + props={storage={}}, adjustments={}, logistics={}, } @@ -192,12 +192,12 @@ local function custom_stockpile(_, keys) end end - -- convert from older parsing style to properties - db_entry.props.max_barrels = db_entry.num_barrels + -- convert to assign()able properties + db_entry.props.storage.max_barrels = db_entry.num_barrels db_entry.num_barrels = nil - db_entry.props.max_bins = db_entry.num_bins + db_entry.props.storage.max_bins = db_entry.num_bins db_entry.num_bins = nil - db_entry.props.max_wheelbarrows = db_entry.num_wheelbarrows + db_entry.props.storage.max_wheelbarrows = db_entry.num_wheelbarrows db_entry.num_wheelbarrows = nil -- alias properties @@ -215,15 +215,15 @@ local function custom_stockpile(_, keys) -- actual properties if props.barrels then - db_entry.props.max_barrels = tonumber(props.barrels) + db_entry.props.storage.max_barrels = tonumber(props.barrels) props.barrels = nil end if props.bins then - db_entry.props.max_bins = tonumber(props.bins) + db_entry.props.storage.max_bins = tonumber(props.bins) props.bins = nil end if props.wheelbarrows then - db_entry.props.max_wheelbarrows = tonumber(props.wheelbarrows) + db_entry.props.storage.max_wheelbarrows = tonumber(props.wheelbarrows) props.wheelbarrows = nil end if props.links_only == 'true' then @@ -268,24 +268,25 @@ local function configure_stockpile(bld, db_entry) end local function init_containers(db_entry, ntiles) - if db_entry.want_barrels or db_entry.props.max_barrels then - local max_barrels = db_entry.props.max_barrels or + local storage = db_entry.props.storage + if db_entry.want_barrels or storage.max_barrels then + local max_barrels = storage.max_barrels or quickfort_set.get_setting('stockpiles_max_barrels') - db_entry.props.max_barrels = (max_barrels < 0 or max_barrels >= ntiles) and ntiles or max_barrels - log('barrels set to %d', db_entry.props.max_barrels) + storage.max_barrels = (max_barrels < 0 or max_barrels >= ntiles) and ntiles or max_barrels + log('barrels set to %d', storage.max_barrels) end - if db_entry.want_bins or db_entry.props.max_bins then - local max_bins = db_entry.props.max_bins or + if db_entry.want_bins or storage.max_bins then + local max_bins = storage.max_bins or quickfort_set.get_setting('stockpiles_max_bins') - db_entry.props.max_bins = (max_bins < 0 or max_bins >= ntiles) and ntiles or max_bins - log('bins set to %d', db_entry.props.max_bins) + storage.max_bins = (max_bins < 0 or max_bins >= ntiles) and ntiles or max_bins + log('bins set to %d', storage.max_bins) end - if db_entry.want_wheelbarrows or db_entry.props.max_wheelbarrows then - local max_wb = db_entry.props.max_wheelbarrows or + if db_entry.want_wheelbarrows or storage.max_wheelbarrows then + local max_wb = storage.max_wheelbarrows or quickfort_set.get_setting('stockpiles_max_wheelbarrows') if max_wb < 0 then max_wb = 1 end - db_entry.props.max_wheelbarrows = (max_wb >= ntiles - 1) and ntiles-1 or max_wb - log('wheelbarrows set to %d', db_entry.props.max_wheelbarrows) + storage.max_wheelbarrows = (max_wb >= ntiles - 1) and ntiles-1 or max_wb + log('wheelbarrows set to %d', storage.max_wheelbarrows) end end @@ -456,7 +457,7 @@ function do_orders(zlevel, grid, ctx) local db_entry = s.db_entry local props = db_entry.props or {} quickfort_orders.enqueue_container_orders(ctx, - props.max_bins, props.max_barrels, props.max_wheelbarrows) + props.storage.max_bins, props.storage.max_barrels, props.storage.max_wheelbarrows) end end diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index dbb2522cf8..0900d57f8d 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -61,11 +61,11 @@ end local function parse_tomb_props(zone_data, props) if props.pets == 'true' then - ensure_keys(zone_data, 'zone_settings', 'tomb').no_pets = false + ensure_keys(zone_data, 'zone_settings', 'tomb', 'flags').no_pets = false props.pets = nil end if props.citizens == 'false' then - ensure_keys(zone_data, 'zone_settings', 'tomb').no_citizens = true + ensure_keys(zone_data, 'zone_settings', 'tomb', 'flags').no_citizens = true props.citizens = nil end end @@ -106,7 +106,7 @@ local zone_db_raw = { b={label='Bedroom', default_data={type=df.civzone_type.Bedroom}}, h={label='Dining Hall', default_data={type=df.civzone_type.DiningHall}}, n={label='Pen/Pasture', default_data={type=df.civzone_type.Pen, - assign={zone_settings={pen={check_occupants=true}}}}}, + assign={zone_settings={pen={flags={check_occupants=true}}}}}}, p={label='Pit/Pond', props_fn=parse_pit_pond_props, default_data={type=df.civzone_type.Pond, assign={zone_settings={pond={flag={keep_filled=true}}}}}}, w={label='Water Source', default_data={type=df.civzone_type.WaterSource}}, @@ -121,7 +121,7 @@ local zone_db_raw = { d={label='Garbage Dump', default_data={type=df.civzone_type.Dump}}, t={label='Animal Training', default_data={type=df.civzone_type.AnimalTraining}}, T={label='Tomb', props_fn=parse_tomb_props, default_data={type=df.civzone_type.Tomb, - assign={zone_settings={tomb={whole=1}}}}}, + assign={zone_settings={tomb={flags={whole=1}}}}}}, g={label='Gather/Pick Fruit', props_fn=parse_gather_props, default_data={type=df.civzone_type.PlantGathering, assign={zone_settings={gather={flags={pick_trees=true, pick_shrubs=true, gather_fallen=true}}}}}}, c={label='Clay', default_data={type=df.civzone_type.ClayCollection}}, diff --git a/lever.lua b/lever.lua index 28cb52e590..39e9be9e53 100644 --- a/lever.lua +++ b/lever.lua @@ -35,6 +35,12 @@ function leverPullInstant(lever) end end +local flag_names = { + [df.building_type.Bridge]={closed="raised", closing="raising", opening="lowering"}, + [df.building_type.Weapon]={closed="retracted", closing="retracting", opening="unretracting"}, +} +setmetatable(flag_names, {__index=function() return {closed="closed", closing="closing", opening="opening"} end}) + function leverDescribe(lever) local lever_name = '' if #lever.name > 0 then @@ -70,23 +76,18 @@ function leverDescribe(lever) for _, m in ipairs(lever.linked_mechanisms) do local tref = dfhack.items.getGeneralRef(m, df.general_ref_type.BUILDING_HOLDER) if tref then - tg = tref:getBuilding() + local tg = tref:getBuilding() if pcall(function() return tg.gate_flags end) then - if tg.gate_flags.closed then - state = "closed" - else - state = "opened" - end + local btype = tg:getType() + state = flag_names[btype].closed - if tg.gate_flags.closing then - state = state .. (', closing (%d)'):format(tg.timer) + if tg.gate_flags[flag_names[btype].closing] then + state = state .. (', %s (%d)'):format(flag_names[btype].closing, tg.timer) + elseif tg.gate_flags[flag_names[btype].opening] then + state = state .. (', %s (%d)'):format(flag_names[btype].opening, tg.timer) end - if tg.gate_flags.opening then - state = state .. (', opening (%d)'):format(tg.timer) - end - end t = t .. diff --git a/load-save.lua b/load-save.lua index 47d5faf46a..1fff6953e0 100644 --- a/load-save.lua +++ b/load-save.lua @@ -12,7 +12,7 @@ if not loadgame_screen then end local found = false for idx, item in ipairs(title_screen.menu_line_id) do - if item == df.viewscreen_titlest.T_menu_line_id.Continue then + if item == df.main_choice_type.Continue then found = true title_screen.sel_menu_line = idx break diff --git a/makeown.lua b/makeown.lua index 5265287253..a0cbae4a71 100644 --- a/makeown.lua +++ b/makeown.lua @@ -46,7 +46,7 @@ local function fix_clothing_ownership(unit) for _, inv_item in ipairs(unit.inventory) do local item = inv_item.item -- only act on worn items, not weapons - if inv_item.mode == df.unit_inventory_item.T_mode.Worn and + if inv_item.mode == df.inv_item_role_type.Worn and not dfhack.items.getOwner(item) and dfhack.items.setOwner(item, unit) then diff --git a/modtools/create-unit.lua b/modtools/create-unit.lua index 8f5b623caf..e612d3d894 100644 --- a/modtools/create-unit.lua +++ b/modtools/create-unit.lua @@ -50,7 +50,7 @@ function createUnit(raceStr, casteStr, pos, locationRange, locationType, age, do if entityRawName and entityRawName~="" then local isValidRawName - for k,v in ipairs(df.global.world.raws.entities) do + for k,v in ipairs(df.global.world.raws.entities.all) do if v.code == entityRawName then isValidRawName = true break @@ -601,7 +601,7 @@ function nameUnit(unit, entityRawName) --choose three random words in the appropriate things local entity_raw if entityRawName and entityRawName~="" then - for k,v in ipairs(df.global.world.raws.entities) do + for k,v in ipairs(df.global.world.raws.entities.all) do if v.code == entityRawName then entity_raw = v break diff --git a/modtools/equip-item.lua b/modtools/equip-item.lua index eca12201ee..ad8e916487 100644 --- a/modtools/equip-item.lua +++ b/modtools/equip-item.lua @@ -94,6 +94,6 @@ if not part then end local mode = args.mode -mode = df.unit_inventory_item.T_mode[mode] --luacheck: retype +mode = df.inv_item_role_type[mode] --luacheck: retype equipItem(unit, item, partId, mode) diff --git a/modtools/item-trigger.lua b/modtools/item-trigger.lua index 5ccb91bd42..290dbc224a 100644 --- a/modtools/item-trigger.lua +++ b/modtools/item-trigger.lua @@ -65,7 +65,7 @@ function compareInvModes(reqMode, itemMode) if reqMode == nil then return end - if not tonumber(reqMode) and df.unit_inventory_item.T_mode[itemMode] == tostring(reqMode) then + if not tonumber(reqMode) and df.inv_item_role_type[itemMode] == tostring(reqMode) then return true elseif tonumber(reqMode) == itemMode then return true @@ -217,7 +217,7 @@ eventful.onUnitAttack.attackTrigger = function(attacker, defender, wound) local attackerWeapon for _, item in ipairs(attacker.inventory) do - if item.mode == df.unit_inventory_item.T_mode.Weapon then + if item.mode == df.inv_item_role_type.Weapon then attackerWeapon = item.item break end diff --git a/modtools/moddable-gods.lua b/modtools/moddable-gods.lua index 5d783f397f..3d1726ba10 100644 --- a/modtools/moddable-gods.lua +++ b/modtools/moddable-gods.lua @@ -77,7 +77,7 @@ godFig.id = df.global.hist_figure_next_id df.global.hist_figure_next_id = 1+df.global.hist_figure_next_id godFig.info = df.historical_figure_info:new() godFig.info.spheres = {new=true} -godFig.info.known_info = df.historical_figure_info.T_known_info:new() +godFig.info.known_info = df.knowledge_profilest:new() godFig.race = race godFig.caste = 0 godFig.sex = gender diff --git a/modtools/pref-edit.lua b/modtools/pref-edit.lua index a8638932fe..471930a7ca 100644 --- a/modtools/pref-edit.lua +++ b/modtools/pref-edit.lua @@ -35,7 +35,7 @@ Valid filters: Include one of these to describe what the id argument represents. - ``-type ``: This describes the type of the preference. Can be entered either using the numerical ID or text id. - Run ``lua @df.unit_preference.T_type`` for a full list of valid values. + Run ``lua @df.unitpref_type`` for a full list of valid values. - ``-subtype ``: The value for an item's subtype - ``-material ``: @@ -267,7 +267,7 @@ function main(...) if args.type and tonumber(args.type) then type = tonumber(args.type) elseif args.type then - type = df.unit_preference.T_type[args.type] + type = df.unitpref_type[args.type] end -- Handle material diff --git a/pref-adjust.lua b/pref-adjust.lua index b06804f6cf..22b3b3b70d 100644 --- a/pref-adjust.lua +++ b/pref-adjust.lua @@ -6,7 +6,7 @@ pss_counter = pss_counter or 31415926 -- --------------------------------------------------------------------------- function insert_preference(unit, preftype, val1) - if preftype == df.unit_preference.T_type.LikeMaterial then + if preftype == df.unitpref_type.LikeMaterial then utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = preftype, @@ -18,7 +18,7 @@ function insert_preference(unit, preftype, val1) prefstring_seed = pss_counter, }, 'prefstring_seed') -- mattype for some is non zero, those non-inorganic like creature:gazelle:hoof is 42,344 - elseif preftype == df.unit_preference.T_type.LikeFood then + elseif preftype == df.unitpref_type.LikeFood then consumable_type = val1[1] consumable_name = val1[2] utils.insert_or_update(unit.status.current_soul.preferences, { @@ -32,7 +32,7 @@ function insert_preference(unit, preftype, val1) active = true, prefstring_seed = pss_counter, }, 'prefstring_seed') - elseif df.unit_preference.T_type[preftype] ~= nil then + elseif df.unitpref_type[preftype] ~= nil then utils.insert_or_update(unit.status.current_soul.preferences, { new = true, type = preftype, @@ -61,68 +61,68 @@ function brainwash_unit(unit, profile) if profile == "IDEAL" then -- Material Likes: IRON(0),STEEL(8),ADAMANTINE(25) - insert_preference(unit, df.unit_preference.T_type.LikeMaterial, "IRON") - insert_preference(unit, df.unit_preference.T_type.LikeMaterial, "STEEL") - -- insert_preference(unit, df.unit_preference.T_type.LikeMaterial, "ADAMANTINE") + insert_preference(unit, df.unitpref_type.LikeMaterial, "IRON") + insert_preference(unit, df.unitpref_type.LikeMaterial, "STEEL") + -- insert_preference(unit, df.unitpref_type.LikeMaterial, "ADAMANTINE") -- Item likes: (WEAPON, ARMOR, SHIELD) - insert_preference(unit, df.unit_preference.T_type.LikeItem, df.item_type.WEAPON) - insert_preference(unit, df.unit_preference.T_type.LikeItem, df.item_type.ARMOR) - insert_preference(unit, df.unit_preference.T_type.LikeItem, df.item_type.SHIELD) + insert_preference(unit, df.unitpref_type.LikeItem, df.item_type.WEAPON) + insert_preference(unit, df.unitpref_type.LikeItem, df.item_type.ARMOR) + insert_preference(unit, df.unitpref_type.LikeItem, df.item_type.SHIELD) -- Plant Likes: "Likes plump helmets for their rounded tops" - insert_preference(unit, df.unit_preference.T_type.LikePlant, dfhack.matinfo.find("MUSHROOM_HELMET_PLUMP:STRUCTURAL").index) - -- insert_preference(unit, df.unit_preference.T_type.LikePlant, dfhack.matinfo.find("PEACH").index) + insert_preference(unit, df.unitpref_type.LikePlant, dfhack.matinfo.find("MUSHROOM_HELMET_PLUMP:STRUCTURAL").index) + -- insert_preference(unit, df.unitpref_type.LikePlant, dfhack.matinfo.find("PEACH").index) -- Prefers to consume drink: (From plump helmets we get dwarven wine) - insert_preference(unit, df.unit_preference.T_type.LikeFood, {df.item_type.DRINK, "MUSHROOM_HELMET_PLUMP:DRINK"}) + insert_preference(unit, df.unitpref_type.LikeFood, {df.item_type.DRINK, "MUSHROOM_HELMET_PLUMP:DRINK"}) -- Prefers to consume food: (plump helmets, mushrooms) - insert_preference(unit, df.unit_preference.T_type.LikeFood, {df.item_type.PLANT, "MUSHROOM_HELMET_PLUMP:MUSHROOM"}) + insert_preference(unit, df.unitpref_type.LikeFood, {df.item_type.PLANT, "MUSHROOM_HELMET_PLUMP:MUSHROOM"}) -- Prefers to consume prepared meals: (quarry bush) - insert_preference(unit, df.unit_preference.T_type.LikeFood, {df.item_type.FOOD, "BUSH_QUARRY"}) + insert_preference(unit, df.unitpref_type.LikeFood, {df.item_type.FOOD, "BUSH_QUARRY"}) -- Creature detests (TROLL, BIRD_BUZZARD, BIRD_VULTURE, CRUNDLE) - insert_preference(unit, df.unit_preference.T_type.HateCreature, list_of_creatures.TROLL) - insert_preference(unit, df.unit_preference.T_type.HateCreature, list_of_creatures.BIRD_BUZZARD) - insert_preference(unit, df.unit_preference.T_type.HateCreature, list_of_creatures.BIRD_VULTURE) - insert_preference(unit, df.unit_preference.T_type.HateCreature, list_of_creatures.CRUNDLE) + insert_preference(unit, df.unitpref_type.HateCreature, list_of_creatures.TROLL) + insert_preference(unit, df.unitpref_type.HateCreature, list_of_creatures.BIRD_BUZZARD) + insert_preference(unit, df.unitpref_type.HateCreature, list_of_creatures.BIRD_VULTURE) + insert_preference(unit, df.unitpref_type.HateCreature, list_of_creatures.CRUNDLE) if #df.global.world.poetic_forms.all > 0 then - insert_preference(unit, df.unit_preference.T_type.LikePoeticForm, 0) -- this just inserts the first song out of typically many. + insert_preference(unit, df.unitpref_type.LikePoeticForm, 0) -- this just inserts the first song out of typically many. end if #df.global.world.musical_forms.all > 0 then - insert_preference(unit, df.unit_preference.T_type.LikeMusicalForm, 0) -- same goes for music + insert_preference(unit, df.unitpref_type.LikeMusicalForm, 0) -- same goes for music end if #df.global.world.dance_forms.all > 0 then - insert_preference(unit, df.unit_preference.T_type.LikeDanceForm, 0) -- and dancing + insert_preference(unit, df.unitpref_type.LikeDanceForm, 0) -- and dancing end -- end IDEAL profile elseif profile == "GOTH" then - insert_preference(unit, df.unit_preference.T_type.LikeMaterial, "CREATURE:DWARF:SKIN") - insert_preference(unit, df.unit_preference.T_type.LikeItem, df.item_type.CORPSE) - insert_preference(unit, df.unit_preference.T_type.LikeItem, df.item_type.CORPSEPIECE) - insert_preference(unit, df.unit_preference.T_type.LikeItem, df.item_type.REMAINS) - insert_preference(unit, df.unit_preference.T_type.LikeItem, df.item_type.COFFIN) - insert_preference(unit, df.unit_preference.T_type.LikeColor, list_of_colors.BLACK) - insert_preference(unit, df.unit_preference.T_type.LikeShape, list_of_shapes.CROSS) - insert_preference(unit, df.unit_preference.T_type.LikePlant, dfhack.matinfo.find("GLUMPRONG").index) - insert_preference(unit, df.unit_preference.T_type.LikeFood, {df.item_type.DRINK, "WEED_RAT:DRINK"}) - insert_preference(unit, df.unit_preference.T_type.LikeFood, {df.item_type.DRINK, "SLIVER_BARB:DRINK"}) - insert_preference(unit, df.unit_preference.T_type.LikeFood, {df.item_type.PLANT, "TUBER_BLOATED:STRUCTURAL"}) - insert_preference(unit, df.unit_preference.T_type.HateCreature, list_of_creatures.ELF) - insert_preference(unit, df.unit_preference.T_type.HateCreature, list_of_creatures.HUMAN) - insert_preference(unit, df.unit_preference.T_type.HateCreature, list_of_creatures.DWARF) + insert_preference(unit, df.unitpref_type.LikeMaterial, "CREATURE:DWARF:SKIN") + insert_preference(unit, df.unitpref_type.LikeItem, df.item_type.CORPSE) + insert_preference(unit, df.unitpref_type.LikeItem, df.item_type.CORPSEPIECE) + insert_preference(unit, df.unitpref_type.LikeItem, df.item_type.REMAINS) + insert_preference(unit, df.unitpref_type.LikeItem, df.item_type.COFFIN) + insert_preference(unit, df.unitpref_type.LikeColor, list_of_colors.BLACK) + insert_preference(unit, df.unitpref_type.LikeShape, list_of_shapes.CROSS) + insert_preference(unit, df.unitpref_type.LikePlant, dfhack.matinfo.find("GLUMPRONG").index) + insert_preference(unit, df.unitpref_type.LikeFood, {df.item_type.DRINK, "WEED_RAT:DRINK"}) + insert_preference(unit, df.unitpref_type.LikeFood, {df.item_type.DRINK, "SLIVER_BARB:DRINK"}) + insert_preference(unit, df.unitpref_type.LikeFood, {df.item_type.PLANT, "TUBER_BLOATED:STRUCTURAL"}) + insert_preference(unit, df.unitpref_type.HateCreature, list_of_creatures.ELF) + insert_preference(unit, df.unitpref_type.HateCreature, list_of_creatures.HUMAN) + insert_preference(unit, df.unitpref_type.HateCreature, list_of_creatures.DWARF) if list_of_creatures.DEMON_1 and df.global.world.raws.creatures.all[list_of_creatures.DEMON_1].prefstring[0] ~= '' then - insert_preference(unit, df.unit_preference.T_type.LikeCreature, list_of_creatures.DEMON_1) + insert_preference(unit, df.unitpref_type.LikeCreature, list_of_creatures.DEMON_1) end if #df.global.world.poetic_forms.all > 0 then - insert_preference(unit, df.unit_preference.T_type.LikePoeticForm, #df.global.world.poetic_forms.all - 1) -- this just inserts the last song out of typically many. + insert_preference(unit, df.unitpref_type.LikePoeticForm, #df.global.world.poetic_forms.all - 1) -- this just inserts the last song out of typically many. end if #df.global.world.musical_forms.all > 0 then - insert_preference(unit, df.unit_preference.T_type.LikeMusicalForm, #df.global.world.musical_forms.all - 1) -- same goes for music + insert_preference(unit, df.unitpref_type.LikeMusicalForm, #df.global.world.musical_forms.all - 1) -- same goes for music end if #df.global.world.dance_forms.all > 0 then - insert_preference(unit, df.unit_preference.T_type.LikeDanceForm, #df.global.world.dance_forms.all - 1) -- and dancing + insert_preference(unit, df.unitpref_type.LikeDanceForm, #df.global.world.dance_forms.all - 1) -- and dancing end -- end GOTH profile else @@ -282,7 +282,7 @@ function get_preferences(unit) print("Preferences for " .. unit_name_to_console(unit) .. ":") for _, pref in ipairs(preferences) do - local pref_type = df.unit_preference.T_type[pref.type] + local pref_type = df.unitpref_type[pref.type] local description = "" if pref_type == "LikeMaterial" then diff --git a/remove-stress.lua b/remove-stress.lua index 76bdee9dac..7cf0193645 100644 --- a/remove-stress.lua +++ b/remove-stress.lua @@ -6,9 +6,9 @@ local utils = require('utils') function removeStress(unit,value) - if unit.counters.soldier_mood > df.unit.T_counters.T_soldier_mood.Enraged then + if unit.counters.soldier_mood > df.soldier_mood_type.Enraged then -- Tantrum, Depressed, or Oblivious - unit.counters.soldier_mood = df.unit.T_counters.T_soldier_mood.None + unit.counters.soldier_mood = df.soldier_mood_type.None end if unit.status.current_soul then if unit.status.current_soul.personality.stress > value then diff --git a/stripcaged.lua b/stripcaged.lua index 2585057c56..a334daba44 100644 --- a/stripcaged.lua +++ b/stripcaged.lua @@ -60,7 +60,7 @@ local function cage_dump_armor(list) if df.general_ref_contains_unitst:is_instance(ref) then local inventory = df.unit.find(ref.unit_id).inventory for _, it in ipairs(inventory) do - if it.mode == df.unit_inventory_item.T_mode.Worn then + if it.mode == df.inv_item_role_type.Worn then count = count + dump_item(it.item) end end @@ -81,7 +81,7 @@ local function cage_dump_weapons(list) if df.general_ref_contains_unitst:is_instance(ref) then local inventory = df.unit.find(ref.unit_id).inventory for _, it in ipairs(inventory) do - if it.mode == df.unit_inventory_item.T_mode.Weapon then + if it.mode == df.inv_item_role_type.Weapon then count = count + dump_item(it.item) end end diff --git a/uniform-unstick.lua b/uniform-unstick.lua index be3a4ad858..73fd78b9aa 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -138,9 +138,9 @@ local function process(unit, args, need_newline) for _, inv_item in ipairs(unit.inventory) do local item = inv_item.item -- Include weapons so we can check we have them later - if inv_item.mode == df.unit_inventory_item.T_mode.Worn or - inv_item.mode == df.unit_inventory_item.T_mode.Weapon or - inv_item.mode == df.unit_inventory_item.T_mode.Strapped + if inv_item.mode == df.inv_item_role_type.Worn or + inv_item.mode == df.inv_item_role_type.Weapon or + inv_item.mode == df.inv_item_role_type.Strapped then worn_items[item.id] = item worn_parts[item.id] = inv_item.body_part_id diff --git a/workorder.lua b/workorder.lua index c72ec99981..32837e7560 100644 --- a/workorder.lua +++ b/workorder.lua @@ -40,11 +40,11 @@ end local used_types = { df.job_type, df.item_type, - df.manager_order.T_frequency, - df.manager_order_condition_item.T_compare_type, - df.manager_order_condition_order.T_condition, + df.workquota_frequency_type, + df.logic_condition_type, + df.workquota_order_condition_type, df.tool_uses, - df.job_art_specification.T_type + df.job_art_specifier_type } local function print_types(_, filter) for _, t in ipairs(used_types) do @@ -265,7 +265,7 @@ function create_orders(orders, quiet) end if it["art"] then - order.art_spec.type = ensure_df_id(df.job_art_specification.T_type, it["art"]["type"]) + order.art_spec.type = ensure_df_id(df.job_art_specifier_type, it["art"]["type"]) or qerror ("Invalid art type value for manager order: " .. it["art"]["type"]) order.art_spec.id = tonumber( it["art"]["id"] ) if it["art"]["subid"] then @@ -278,7 +278,7 @@ function create_orders(orders, quiet) --order.status.validated = it["is_validated"] -- ignoring --order.status.active = it["is_active"] -- ignoring - order.frequency = ensure_df_id(df.manager_order.T_frequency, it["frequency"]) + order.frequency = ensure_df_id(df.workquota_frequency_type, it["frequency"]) or qerror("Invalid frequency value for manager order: " .. it["frequency"]) -- finished_year, finished_year_tick @@ -300,7 +300,7 @@ function create_orders(orders, quiet) condition = df.manager_order_condition_item:new() dfhack.with_onerror(function() condition:delete() end, -- cleanup in case of errors function() - condition.compare_type = ensure_df_id(df.manager_order_condition_item.T_compare_type, it2["condition"]) + condition.compare_type = ensure_df_id(df.logic_condition_type, it2["condition"]) or qerror ("Invalid item condition for manager order: " .. it2["condition"] ) condition.compare_val = tonumber(it2["value"]) @@ -386,7 +386,7 @@ function create_orders(orders, quiet) condition.order_id = id ~= it["id"] and id_mapping[id] or qerror("Missing order condition target for manager order: " .. it2["order"]) - condition.condition = ensure_df_id(df.manager_order_condition_order.T_condition, it2["condition"]) + condition.condition = ensure_df_id(df.workquota_order_condition_type, it2["condition"]) or qerror ( "Invalid order condition type for manager order: " .. it2["condition"] ) -- condition.unk_1 From f861ec210deb33b713b82b6f8490f5766dde8f8e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Feb 2025 17:57:55 -0800 Subject: [PATCH 415/811] fix jjob_role_type typo --- autocheese.lua | 2 +- gui/advfort.lua | 6 +++--- immortal-cravings.lua | 4 ++-- internal/advfort/advfort_items.lua | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/autocheese.lua b/autocheese.lua index c857b3b57e..0e9fb52215 100644 --- a/autocheese.lua +++ b/autocheese.lua @@ -18,7 +18,7 @@ function makeCheese(barrel, workshop) jitem.flags1.milk = true job.job_items.elements:insert('#', jitem) - if not dfhack.job.attachJobItem(job, barrel, df.jjob_role_type.Reagent, 0, -1) then + if not dfhack.job.attachJobItem(job, barrel, df.job_role_type.Reagent, 0, -1) then dfhack.error('could not attach item') end diff --git a/gui/advfort.lua b/gui/advfort.lua index 9560b52bca..7e3b6298c3 100644 --- a/gui/advfort.lua +++ b/gui/advfort.lua @@ -872,7 +872,7 @@ function find_suitable_items(job,items,job_items) if not settings.gui_item_select then if (item_counts[job_id]>0 and item_suitable) or settings.build_by_items then --cur_item.flags.in_job=true - job.items:insert("#",{new=true,item=cur_item,role=df.jjob_role_type.Reagent,job_item_idx=job_id}) + job.items:insert("#",{new=true,item=cur_item,role=df.job_role_type.Reagent,job_item_idx=job_id}) item_counts[job_id]=item_counts[job_id]-cur_item:getTotalDimension() --print(string.format("item added, job_item_id=%d, item %s, quantity left=%d",job_id,tostring(cur_item),item_counts[job_id])) used_item_id[cur_item.id]=true @@ -1021,8 +1021,8 @@ end -- print("AAA FAILED!") -- return false -- end --- args.job.items[0].role=df.jjob_role_type.LinkToTarget --- args.job.items[1].role=df.jjob_role_type.LinkToTrigger +-- args.job.items[0].role=df.job_role_type.LinkToTarget +-- args.job.items[1].role=df.job_role_type.LinkToTrigger -- end function fake_linking(lever,building,slots) local item1=slots[1].items[1] diff --git a/immortal-cravings.lua b/immortal-cravings.lua index a74d5aeb33..2b76ee4646 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -74,7 +74,7 @@ local function goDrink(unit) job.flags.special = true local dx, dy, dz = dfhack.items.getPosition(drink) job.pos = xyz2pos(dx, dy, dz) - if not dfhack.job.attachJobItem(job, drink, df.jjob_role_type.Other, -1, -1) then + if not dfhack.job.attachJobItem(job, drink, df.job_role_type.Other, -1, -1) then error('could not attach drink') return end @@ -96,7 +96,7 @@ local function goEat(unit) job.flags.special = true local dx, dy, dz = dfhack.items.getPosition(meal) job.pos = xyz2pos(dx, dy, dz) - if not dfhack.job.attachJobItem(job, meal, df.jjob_role_type.Other, -1, -1) then + if not dfhack.job.attachJobItem(job, meal, df.job_role_type.Other, -1, -1) then error('could not attach meal') return end diff --git a/internal/advfort/advfort_items.lua b/internal/advfort/advfort_items.lua index 4a50fe264b..32131521c3 100644 --- a/internal/advfort/advfort_items.lua +++ b/internal/advfort/advfort_items.lua @@ -189,7 +189,7 @@ function jobitemEditor:commit() for _,slot in pairs(self.slots) do if slot.id == orderedSlotID then for _1,cur_item in pairs(slot.items) do - self.job.items:insert("#",{new=true,item=cur_item,role=df.jjob_role_type.Reagent,job_item_idx=slot.id}) + self.job.items:insert("#",{new=true,item=cur_item,role=df.job_role_type.Reagent,job_item_idx=slot.id}) end end end From 766c6b27ed3cb6774617d340dcf603b49ca775e6 Mon Sep 17 00:00:00 2001 From: velanos Date: Sun, 9 Feb 2025 14:24:26 +0100 Subject: [PATCH 416/811] Update advfort.lua (#1394) use new API to get adventurer and focus strings --- gui/advfort.lua | 42 +++++++++++++++++++++--------------------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/gui/advfort.lua b/gui/advfort.lua index 125bed7ba8..644c9c9433 100644 --- a/gui/advfort.lua +++ b/gui/advfort.lua @@ -97,7 +97,7 @@ function reverseRaceLookup(id) end function deon_filter(name,type_id,subtype_id,custom_id, parent) --print(name) - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local race_filter=build_filter[reverseRaceLookup(adv.race)] if race_filter then if race_filter.forbid_all then @@ -195,7 +195,7 @@ end function advGlobalPos() local map=df.global.world.map local wd=df.global.world.world_data - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() --wd.midmap_data.adv_region_x*16+wd.midmap_data.adv_emb_x,wd.midmap_data.adv_region_y*16+wd.midmap_data.adv_emb_y --return wd.midmap_data.adv_region_x*16+wd.midmap_data.adv_emb_x,wd.midmap_data.adv_region_y*16+wd.midmap_data.adv_emb_y --return wd.midmap_data.adv_region_x*16+wd.midmap_data.adv_emb_x+adv.pos.x/16,wd.midmap_data.adv_region_y*16+wd.midmap_data.adv_emb_y+adv.pos.y/16 @@ -1178,7 +1178,7 @@ usetool=defclass(usetool,gui.Screen) usetool.focus_path = 'advfort' --luacheck: out=string function usetool:getModeName() - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local ret if adv.job.current_job then ret= string.format("%s working(%d) ",(actions[(mode or 0)+1][1] or ""),adv.job.current_job.completion_timer) @@ -1232,7 +1232,7 @@ function usetool:init(args) } } } - local labors=df.global.world.units.active[0].status.labors + local labors=dfhack.world.getAdventurer().status.labors for i,v in ipairs(labors) do labors[i]=true end @@ -1302,7 +1302,7 @@ function siegeWeaponActionChosen(args,actionid) args.pre_actions={dfhack.curry(setFiltersUp,{items={{quantity=1,item_type=df.SIEGEAMMO}}}),AssignJobItems} end args.job_type=action - args.unit=df.global.world.units.active[0] + args.unit=dfhack.world.getAdventurer() local from_pos={x=args.unit.pos.x,y=args.unit.pos.y, z=args.unit.pos.z} args.from_pos=from_pos args.pos=from_pos @@ -1312,7 +1312,7 @@ function siegeWeaponActionChosen(args,actionid) action=df.job_type.FireCatapult end args.job_type=action - args.unit=df.global.world.units.active[0] + args.unit=dfhack.world.getAdventurer() local from_pos={x=args.unit.pos.x,y=args.unit.pos.y, z=args.unit.pos.z} args.from_pos=from_pos args.pos=from_pos @@ -1329,7 +1329,7 @@ function putItemToBuilding(building,item) end end function usetool:openPutWindow(building) - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local items=EnumItems{pos=adv.pos,unit=adv, inv={[df.unit_inventory_item.T_mode.Hauled]=true,--[df.unit_inventory_item.T_mode.Worn]=true, [df.unit_inventory_item.T_mode.Weapon]=true,},deep=true} @@ -1345,7 +1345,7 @@ function usetool:openSiegeWindow(building) dfhack.curry(siegeWeaponActionChosen,args)) end function usetool:onWorkShopButtonClicked(building,index,choice) - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local args={unit=adv,building=building} if df.interface_button_building_new_jobst:is_instance(choice.button) then choice.button:click() @@ -1393,7 +1393,7 @@ function usetool:openShopWindowButtoned(building,no_reset) ,nil, nil,true) end function usetool:openShopWindow(building) - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local filter_pile=workshopJobs.getJobs(building:getType(),building:getSubtype(),building:getCustomType()) if filter_pile then @@ -1441,7 +1441,7 @@ function track_stop_configure(bld) --TODO: dedicated widget with nice interface dialog.showListPrompt("Track stop configure", "Choose what to change:",COLOR_WHITE,choices,chosen) end function usetool:armCleanTrap(building) - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() --[[ Lever, PressurePlate, @@ -1487,7 +1487,7 @@ function usetool:armCleanTrap(building) end --luacheck: in=df.building_hivest out=none function usetool:hiveActions(building) - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local args={unit=adv,post_actions={AssignBuildingRef},pos=adv.pos, from_pos=adv.pos,job_type=df.job_type.InstallColonyInHive,building=building,screen=self} local job_filter={items={{quantity=1,item_type=df.item_type.VERMIN}} } @@ -1498,14 +1498,14 @@ function usetool:hiveActions(building) end function usetool:operatePump(building) --TODO: low priotity, but would be nice to have the job auto cleanup (i.e. one work would only pump and then you could press it again) - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local set_operate=function ( args ) args.building.pump_manually=true end makeJob{unit=adv,building=building,post_actions={AssignBuildingRef,set_operate},pos=adv.pos,from_pos=adv.pos,job_type=df.job_type.OperatePump,screen=self} end function usetool:farmPlot(building) - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local do_harvest=false for id, con_item in pairs(building.contained_items) do if con_item.use_mode==2 and con_item.item:getType()==df.item_type.PLANT then @@ -1531,14 +1531,14 @@ function usetool:farmPlot(building) end --luacheck: in=df.building_bedst out=none function usetool:bedActions(building) - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local args={unit=adv,pos=adv.pos,from_pos=adv.pos,screen=self,building=building, job_type=df.job_type.Sleep,post_actions={AssignBuildingRef}} makeJob(args) end --luacheck: in=df.building_chairst out=none function usetool:chairActions(building) - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local eatjob={items={{quantity=1,item_type=df.item_type.FOOD}}} local args={unit=adv,pos=adv.pos,from_pos=adv.pos,screen=self,job_type=df.job_type.Eat,building=building, pre_actions={dfhack.curry(setFiltersUp,eatjob),AssignJobItems},post_actions={AssignBuildingRef}} @@ -1640,7 +1640,7 @@ end function usetool:setupFields() local ui=df.global.plotinfo - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() if settings.set_civ==true then ui.civ_id=adv.civ_id ui.race_id=adv.race @@ -1676,7 +1676,7 @@ function usetool:siteCheck() end --movement and co... Also passes on allowed keys function usetool:fieldInput(keys) - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local cur_mode=actions[(mode or 0)+1] local failed=false for code,_ in pairs(keys) do @@ -1744,7 +1744,7 @@ function usetool:onInput(keys) self:update_site() - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() if keys.LEAVESCREEN then if df.global.cursor.x~=-30000 then --if not poiting at anything @@ -1786,7 +1786,7 @@ function usetool:cancel_wait() self.long_wait=false end function usetool:onIdle() - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local job_ptr=adv.job.current_job local job_action=findAction(adv,df.unit_action_type.Job) @@ -1825,7 +1825,7 @@ function usetool:onIdle() self._native.parent:logic() end function usetool:isOnBuilding() - local adv=df.global.world.units.active[0] + local adv=dfhack.world.getAdventurer() local bld=dfhack.buildings.findAtTile(adv.pos) if bld and MODES[bld:getType()]~=nil and bld:getBuildStage()==bld:getMaxBuildStage() then return true,MODES[bld:getType()],bld @@ -1837,7 +1837,7 @@ function usetool:onRenderBody(dc) self:shopMode(self:isOnBuilding()) self:renderParent() end -if not (dfhack.gui.getCurFocus()=="dungeonmode/Look" or dfhack.gui.getCurFocus()=="dungeonmode/Default") then +if not (dfhack.gui.matchFocusString('dungeonmode/Look') or dfhack.gui.matchFocusString('dungeonmode/Default')) then qerror("This script requires an adventurer mode with (l)ook or default mode.") end usetool():show() From a6d26cd795a8f858bb714f199ed54e62b77085af Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 12:22:44 -0800 Subject: [PATCH 417/811] colorize moody dwarf warnings --- changelog.txt | 1 + internal/notify/notifications.lua | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 9ccf56e1ed..4e53f12d9b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -39,6 +39,7 @@ Template for new versions: ## Misc Improvements - `hide-tutorials`: if enabled, also hide tutorial popups for adventure mode - `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game +- `gui/notify`: moody dwarf notification turns red when they can't find workshop or items ## Removed diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index e992feb2e7..b0d8b2bcd4 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -428,7 +428,7 @@ NOTIFICATIONS_BY_IDX = { if dfhack.buildings.findAtTile(unit.path.dest) then message = 'moody dwarf is claiming a workshop' else - message = 'moody dwarf can\'t find needed workshop!' + message = {{text='moody dwarf can\'t find needed workshop!', pen=COLOR_LIGHTRED}} end elseif job.flags.fetching or job.flags.bringing or unit.path.goal == df.unit_path_goal.None @@ -437,7 +437,7 @@ NOTIFICATIONS_BY_IDX = { elseif job.flags.working then message = 'moody dwarf is working' else - message = 'moody dwarf can\'t find needed item!' + message = {{text='moody dwarf can\'t find needed item!', pen=COLOR_LIGHTRED}} end return true end) From 67141851c59bb3d2ac08f48720123c3239f45521 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 17:06:13 -0800 Subject: [PATCH 418/811] finish updating scripts for the Great Reorg --- add-thought.lua | 2 +- assign-preferences.lua | 18 +++---- devel/export-dt-ini.lua | 20 +++---- devel/export-map.lua | 2 +- devel/tree-info.lua | 68 +++++++++++++++++------- docs/fix/population-cap.rst | 2 +- dwarf-op.lua | 2 +- exportlegends.lua | 10 ++-- fix/population-cap.lua | 33 ++++++------ fix/wildlife.lua | 2 +- gui/autogems.lua | 4 +- gui/dfstatus.lua | 2 +- gui/sandbox.lua | 2 +- gui/tiletypes.lua | 2 +- gui/unit-syndromes.lua | 6 +-- internal/caravan/common.lua | 2 +- internal/caravan/predicates.lua | 2 +- internal/notify/notifications.lua | 2 +- internal/quickfort/stockflow.lua | 2 +- launch.lua | 2 +- locate-ore.lua | 6 +-- makeown.lua | 4 +- modtools/add-syndrome.lua | 2 +- modtools/create-unit.lua | 4 +- modtools/reaction-trigger-transition.lua | 2 +- modtools/reaction-trigger.lua | 2 +- modtools/syndrome-trigger.lua | 2 +- pref-adjust.lua | 2 +- region-pops.lua | 2 +- starvingdead.lua | 2 +- workorder.lua | 2 +- 31 files changed, 123 insertions(+), 92 deletions(-) diff --git a/add-thought.lua b/add-thought.lua index 40228356c1..b68916d30b 100644 --- a/add-thought.lua +++ b/add-thought.lua @@ -14,7 +14,7 @@ function addEmotionToUnit(unit,thought,emotion,severity,strength,subthought) local properThought = tonumber(thought) or df.unit_thought_type[thought] local properSubthought = tonumber(subthought) if not properThought or not df.unit_thought_type[properThought] then - for _,syn in ipairs(df.global.world.raws.syndromes.all) do + for _,syn in ipairs(df.global.world.raws.mat_table.syndromes.all) do if syn.syn_name == thought then properThought = df.unit_thought_type.Syndrome properSubthought = syn.id diff --git a/assign-preferences.lua b/assign-preferences.lua index 86cc3fbf2f..9a8c1bdc6a 100644 --- a/assign-preferences.lua +++ b/assign-preferences.lua @@ -89,7 +89,7 @@ local preference_functions = { mattype = mat_info.type, matindex = mat_info.index, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -122,7 +122,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -199,7 +199,7 @@ local preference_functions = { mattype = mat_info.type, matindex = mat_info.index, mat_state = 1, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } end @@ -234,7 +234,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -275,7 +275,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } end @@ -310,7 +310,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -343,7 +343,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -377,7 +377,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else @@ -410,7 +410,7 @@ local preference_functions = { mattype = -1, matindex = -1, mat_state = 0, - active = true, + flags = {visible = true}, prefstring_seed = rng:random() } else diff --git a/devel/export-dt-ini.lua b/devel/export-dt-ini.lua index e1ea4439b7..cc58277e54 100644 --- a/devel/export-dt-ini.lua +++ b/devel/export-dt-ini.lua @@ -92,7 +92,7 @@ address('musical_forms_vector',globals,'world','musical_forms','all') address('dance_forms_vector',globals,'world','dance_forms','all') address('occupations_vector',globals,'world','occupations','all') address('world_data',globals,'world','world_data') -address('material_templates_vector',globals,'world','raws','material_templates') +address('material_templates_vector',globals,'world','raws','material_templates','all') address('inorganics_vector',globals,'world','raws','inorganics') address('plants_vector',globals,'world','raws','plants','all') address('races_vector',globals,'world','raws','creatures','all') @@ -255,14 +255,14 @@ address('pants_armor_properties',df.itemdef_pantsst,'props') address('other_armor_properties',df.itemdef_helmst,'props') header('material_offsets') -address('solid_name',df.material_common,'state_name','Solid') -address('liquid_name',df.material_common,'state_name','Liquid') -address('gas_name',df.material_common,'state_name','Gas') -address('powder_name',df.material_common,'state_name','Powder') -address('paste_name',df.material_common,'state_name','Paste') -address('pressed_name',df.material_common,'state_name','Pressed') -address('flags',df.material_common,'flags') -address('reaction_class',df.material_common,'reaction_class') +address('solid_name',df.material,'state_name','Solid') +address('liquid_name',df.material,'state_name','Liquid') +address('gas_name',df.material,'state_name','Gas') +address('powder_name',df.material,'state_name','Powder') +address('paste_name',df.material,'state_name','Paste') +address('pressed_name',df.material,'state_name','Pressed') +address('flags',df.material,'flags') +address('reaction_class',df.material,'reaction_class') address('prefix',df.material,'prefix') address('inorganic_materials_vector',df.inorganic_raw,'material') address('inorganic_flags',df.inorganic_raw,'flags') @@ -320,7 +320,7 @@ address('size_info',df.unit,'body','size_info','size_cur') address('size_base',df.unit,'body','size_info','size_base') address('curse',df.unit,'curse','name') address('curse_add_flags1',df.unit,'curse','add_tags1') -address('turn_count',df.unit,'curse','time_on_site') +address('turn_count',df.unit,'curse','interaction','time_on_site') address('souls',df.unit,'status','souls') address('states',df.unit,'status','misc_traits') address('labors',df.unit,'status','labors') diff --git a/devel/export-map.lua b/devel/export-map.lua index 2fc9efd523..d01844a54b 100644 --- a/devel/export-map.lua +++ b/devel/export-map.lua @@ -158,7 +158,7 @@ local function setup_keys(options) KEYS.MATERIAL.STONE = {} KEYS.MATERIAL.GEM = {} - for id, rock in ipairs(df.global.world.raws.inorganics) do + for id, rock in ipairs(df.global.world.raws.inorganics.all) do local material = rock.material local name = material.state_adj.Solid KEYS.MATERIAL.SOLID[id] = name diff --git a/devel/tree-info.lua b/devel/tree-info.lua index 4bd9811ed5..7bf4b42e41 100644 --- a/devel/tree-info.lua +++ b/devel/tree-info.lua @@ -1,29 +1,57 @@ --Print a tree_info visualization of the tree at the cursor. --@module = true -local branch_dir = -{ - [0] = ' ', - [1] = string.char(26), --W - [2] = string.char(25), --N - [3] = string.char(217), --WN - [4] = string.char(27), --E - [5] = string.char(196), --WE - [6] = string.char(192), --NE - [7] = string.char(193), --WNE - [8] = string.char(24), --S - [9] = string.char(191), --WS - [10] = string.char(179), --NS - [11] = string.char(180), --WNS - [12] = string.char(218), --ES - [13] = string.char(194), --WES - [14] = string.char(195), --NES - [15] = string.char(197), --WNES +-- [w][n][e][s] +local branch_chars = { + [true]={ + [true]={ + [true]={ + [true]=string.char(197), --WNES + [false]=string.char(193), --WNE + }, + [false]={ + [true]=string.char(180), --WNS + [false]=string.char(217), --WN + }, + }, + [false]={ + [true]={ + [true]=string.char(194), --WES + [false]=string.char(196), --WE + }, + [false]={ + [true]=string.char(191), --WS + [false]=string.char(26), --W + }, + }, + }, + [false]={ + [true]={ + [true]={ + [true]=string.char(195), --NES + [false]=string.char(192), --NE + }, + [false]={ + [true]=string.char(179), --NS + [false]=string.char(25), --N + }, + }, + [false]={ + [true]={ + [true]=string.char(218), --ES + [false]=string.char(27), --E + }, + [false]={ + [true]=string.char(24), --S + [false]=' ', + }, + }, + }, } local function print_color(s, color) dfhack.color(color) - dfhack.print(s) + dfhack.print(dfhack.df2console(s)) dfhack.color(COLOR_RESET) end @@ -68,7 +96,7 @@ function printTreeTile(bits) end chars = chars-2 - print_color(' '..(branch_dir[bits.branches_dir] or '?'), COLOR_GREY) + print_color(' '..(branch_chars[bits.branch_w][bits.branch_n][bits.branch_e][bits.branch_s] or '?'), COLOR_GREY) local dir = bits.parent_dir if dir > 0 then diff --git a/docs/fix/population-cap.rst b/docs/fix/population-cap.rst index c1ebb9ae04..a2eed10bdd 100644 --- a/docs/fix/population-cap.rst +++ b/docs/fix/population-cap.rst @@ -5,7 +5,7 @@ fix/population-cap :summary: Ensure the population cap is respected. :tags: fort bugfix -Run this if you continue to get migrant wave even after you have exceeded your +Run this if you continue to get migrant waves even after you have exceeded your set population cap. The reason this tool is needed is that the game only updates the records of your diff --git a/dwarf-op.lua b/dwarf-op.lua index 207a481b84..3581e3004c 100644 --- a/dwarf-op.lua +++ b/dwarf-op.lua @@ -733,7 +733,7 @@ local seasons = { 'winter', } function GetWave(dwf) - arrival_time = current_tick - dwf.curse.time_on_site; + arrival_time = current_tick - dwf.curse.interaction.time_on_site; --print(string.format("Current year %s, arrival_time = %s, ticks_per_year = %s", df.global.cur_year, arrival_time, ticks_per_year)) arrival_year = df.global.cur_year + (arrival_time // ticks_per_year); arrival_season = 1 + (arrival_time % ticks_per_year) // ticks_per_season; diff --git a/exportlegends.lua b/exportlegends.lua index 3e65abd68b..5ac72a0511 100644 --- a/exportlegends.lua +++ b/exportlegends.lua @@ -784,16 +784,16 @@ local function export_more_legends_xml() file:write("\t\t"..(world.raws.creatures.all[event.race].caste[v].caste_id):lower().."\n") end elseif k == "interaction" and df.history_event_hf_does_interactionst:is_instance(event) then - if #world.raws.interactions[v].sources > 0 then - local str_1 = world.raws.interactions[v].sources[0].hist_string_1 + if #world.raws.interactions.all[v].sources > 0 then + local str_1 = world.raws.interactions.all[v].sources[0].hist_string_1 if string.sub (str_1, 1, 1) == " " and string.sub (str_1, string.len (str_1), string.len (str_1)) == " " then str_1 = string.sub (str_1, 2, string.len (str_1) - 1) end - file:write("\t\t"..str_1..world.raws.interactions[v].sources[0].hist_string_2.."\n") + file:write("\t\t"..str_1..world.raws.interactions.all[v].sources[0].hist_string_2.."\n") end elseif k == "interaction" and df.history_event_hf_learns_secretst:is_instance(event) then - if #world.raws.interactions[v].sources > 0 then - file:write("\t\t"..world.raws.interactions[v].sources[0].name.."\n") + if #world.raws.interactions.all[v].sources > 0 then + file:write("\t\t"..world.raws.interactions.all[v].sources[0].name.."\n") end elseif df.history_event_hist_figure_diedst:is_instance(event) and k == "weapon" then for detailK,detailV in pairs(v) do diff --git a/fix/population-cap.lua b/fix/population-cap.lua index a68743b906..afa5ead765 100644 --- a/fix/population-cap.lua +++ b/fix/population-cap.lua @@ -1,27 +1,30 @@ -local ui = df.global.plotinfo -local ui_stats = ui.tasks -local civ = df.historical_entity.find(ui.civ_id) +local plotinfo = df.global.plotinfo +local tasks = plotinfo.tasks +local knowledge = tasks.knowledge -if not civ then +local civ = df.historical_entity.find(plotinfo.civ_id) + +if not dfhack.isMapLoaded() or not dfhack.world.isFortressMode() or not civ then qerror('No active fortress.') end local civ_stats = civ.activity_stats -if not civ_stats then - civ.activity_stats = { - new = true, - created_weapons = { resize = #ui_stats.created_weapons }, - discovered_creature_foods = { resize = #ui_stats.discovered_creature_foods }, - discovered_creatures = { resize = #ui_stats.discovered_creatures }, - discovered_plant_foods = { resize = #ui_stats.discovered_plant_foods }, - discovered_plants = { resize = #ui_stats.discovered_plants }, - } - civ_stats = civ.activity_stats +local function upsize(civ_vec, tasks_vec) + local tasks_vec_size = #tasks_vec + if #civ_vec < tasks_vec_size then + civ_vec:resize(tasks_vec_size) + end end +upsize(civ_stats.created_weapons, tasks.created_weapons) +upsize(civ_stats.knowledge.discovered_creature_foods, knowledge.discovered_creature_foods) +upsize(civ_stats.knowledge.discovered_creatures, knowledge.discovered_creatures) +upsize(civ_stats.knowledge.discovered_plant_foods, knowledge.discovered_plant_foods) +upsize(civ_stats.knowledge.discovered_plants, knowledge.discovered_plants) + -- Use max to keep at least some of the original caravan communication idea -local new_pop = math.max(civ_stats.population, ui_stats.population) +local new_pop = math.max(civ_stats.population, tasks.population) if civ_stats.population ~= new_pop then civ_stats.population = new_pop diff --git a/fix/wildlife.lua b/fix/wildlife.lua index c0d199259a..d5fc492815 100644 --- a/fix/wildlife.lua +++ b/fix/wildlife.lua @@ -46,7 +46,7 @@ end local function refund_population(entry) local epop = entry.pop - for _,population in ipairs(df.global.world.populations) do + for _,population in ipairs(df.global.world.populations.all) do local wpop = population.population if population.quantity < 10000001 and wpop.region_x == epop.region_x and diff --git a/gui/autogems.lua b/gui/autogems.lua index b0665c994c..3d84c6545d 100644 --- a/gui/autogems.lua +++ b/gui/autogems.lua @@ -39,7 +39,7 @@ CONFIG_KEY = "autogems/config" blacklist = {} gems = {} -for id, raw in pairs(df.global.world.raws.inorganics) do +for id, raw in pairs(df.global.world.raws.inorganics.all) do if raw.material.flags.IS_GEM then if blacklist[id] == nil then blacklist[id] = false @@ -97,7 +97,7 @@ end function save() local save_blacklist = {} - for id in ipairs(df.global.world.raws.inorganics) do + for id in ipairs(df.global.world.raws.inorganics.all) do if blacklist[id] then table.insert(save_blacklist, id) end diff --git a/gui/dfstatus.lua b/gui/dfstatus.lua index 0349c0e06c..25c073a51f 100644 --- a/gui/dfstatus.lua +++ b/gui/dfstatus.lua @@ -31,7 +31,7 @@ config = { function parse_config() local metal_map = {} - for id, raw in pairs(df.global.world.raws.inorganics) do + for id, raw in pairs(df.global.world.raws.inorganics.all) do if raw.material.flags.IS_METAL then metal_map[raw.id:upper()] = id metal_map[id] = raw.id:upper() diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 8418c79377..29e93b51ce 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -238,7 +238,7 @@ end function Sandbox:find_zombie_syndrome() if self.zombie_syndrome then return self.zombie_syndrome end - for _,syn in ipairs(df.global.world.raws.syndromes.all) do + for _,syn in ipairs(df.global.world.raws.mat_table.syndromes.all) do local has_flags, has_flash = false, false for _,effect in ipairs(syn.ce) do if df.creature_interaction_effect_display_namest:is_instance(effect) then diff --git a/gui/tiletypes.lua b/gui/tiletypes.lua index c568ad8fa3..193f1ca0bb 100644 --- a/gui/tiletypes.lua +++ b/gui/tiletypes.lua @@ -1739,7 +1739,7 @@ function TiletypeScreen:generateDataLists() data_lists.stone_list = { { text = "none", value = -1 } } data_lists.stone_dict = { [-1] = { label= "NONE", value= -1, pen= itemColor("NONE") } } - for i,mat in ipairs(df.global.world.raws.inorganics) do + for i,mat in ipairs(df.global.world.raws.inorganics.all) do if mat and mat.material and not mat.flags[df.inorganic_flags.SOIL_ANY] and not mat.material.flags[df.material_flags.IS_METAL] diff --git a/gui/unit-syndromes.lua b/gui/unit-syndromes.lua index b789bfd97d..52830f0383 100644 --- a/gui/unit-syndromes.lua +++ b/gui/unit-syndromes.lua @@ -256,7 +256,7 @@ local function getSyndromeName(syndrome_raw) end local function getSyndromeEffects(syndrome_type) - local syndrome_raw = df.global.world.raws.syndromes.all[syndrome_type] + local syndrome_raw = df.global.world.raws.mat_table.syndromes.all[syndrome_type] local syndrome_effects = {} for _, effect in ipairs(syndrome_raw.ce) do @@ -442,7 +442,7 @@ function UnitSyndromes:showUnits(_, choice) local choices = {} if choice.text == "All syndromes" then - for _, syndrome in pairs(df.global.world.raws.syndromes.all) do + for _, syndrome in pairs(df.global.world.raws.mat_table.syndromes.all) do if #syndrome.ce == 0 then goto skipsyndrome end @@ -491,7 +491,7 @@ function UnitSyndromes:showUnitSyndromes(index, choice) end for _, syndrome in pairs(unit_syndromes) do - local syndrome_raw = df.global.world.raws.syndromes.all[syndrome.type] + local syndrome_raw = df.global.world.raws.mat_table.syndromes.all[syndrome.type] if #syndrome_raw.ce == 0 then goto skipsyndrome diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index ec53bd585e..d0974c91f5 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -334,7 +334,7 @@ end function get_banned_items() local banned_items = {} - for _, mandate in ipairs(df.global.world.mandates) do + for _, mandate in ipairs(df.global.world.mandates.all) do if mandate.mode == df.mandate_type.Export then register_item_type(banned_items, mandate) end diff --git a/internal/caravan/predicates.lua b/internal/caravan/predicates.lua index 6780e46e55..695a9db06c 100644 --- a/internal/caravan/predicates.lua +++ b/internal/caravan/predicates.lua @@ -11,7 +11,7 @@ end local PREDICATE_LIBRARY = { {name='weapons-grade metal', match=function(item) if item:getMaterial() ~= 0 then return false end - local flags = df.global.world.raws.inorganics[item:getMaterialIndex()].material.flags + local flags = df.global.world.raws.inorganics.all[item:getMaterialIndex()].material.flags return flags.IS_METAL and (flags.ITEMS_METAL or flags.ITEMS_WEAPON or flags.ITEMS_WEAPON_RANGED or flags.ITEMS_AMMO or flags.ITEMS_ARMOR) end}, diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 7990149712..d51d930100 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -383,7 +383,7 @@ NOTIFICATIONS_BY_IDX = { default=true, dwarf_fn=function() local count = 0 - for _, mandate in ipairs(df.global.world.mandates) do + for _, mandate in ipairs(df.global.world.mandates.all) do if mandate.mode == df.mandate_type.Make and mandate.timeout_limit - mandate.timeout_counter < 2500 then diff --git a/internal/quickfort/stockflow.lua b/internal/quickfort/stockflow.lua index 42b54e148b..098297a951 100644 --- a/internal/quickfort/stockflow.lua +++ b/internal/quickfort/stockflow.lua @@ -116,7 +116,7 @@ function collect_reactions() reaction_entry(result, job_types.CatchLiveFish) -- Cutting, encrusting, and metal extraction. - local rock_types = df.global.world.raws.inorganics + local rock_types = df.global.world.raws.inorganics.all for rock_id = #rock_types-1, 0, -1 do local material = rock_types[rock_id].material local rock_name = material.state_adj.Solid diff --git a/launch.lua b/launch.lua index 763eace9ad..57cf1ef40d 100644 --- a/launch.lua +++ b/launch.lua @@ -20,7 +20,7 @@ function launch(unitSource,unitRider) local count=0 - local l = df.global.world.proj_list + local l = df.global.world.projectiles.all local lastlist=l l=l.next while l do diff --git a/locate-ore.lua b/locate-ore.lua index a9e027fbbf..4bad5386ef 100644 --- a/locate-ore.lua +++ b/locate-ore.lua @@ -49,7 +49,7 @@ end local function matchesMetalOreById(mat_indices, target_ore) for _, mat_index in ipairs(mat_indices) do - local metal_raw = df.global.world.raws.inorganics[mat_index] + local metal_raw = df.global.world.raws.inorganics.all[mat_index] if metal_raw ~= nil and string.lower(metal_raw.id) == target_ore then return true end @@ -77,7 +77,7 @@ local function findOres(opts, check_designation, target_ore) goto skipevent end - local ino_raw = df.global.world.raws.inorganics[bevent.inorganic_mat] + local ino_raw = df.global.world.raws.inorganics.all[bevent.inorganic_mat] if not ino_raw.flags.METAL_ORE then goto skipevent end @@ -131,7 +131,7 @@ local function getOreDescription(opts, vein) local visible = opts.all and '' or 'visible ' local str = ('%5d %stile(s) of %s ('):format(#vein.positions, visible, tostring(vein.inorganic_id):lower()) for _, mat_index in ipairs(vein.metal_ore.mat_index) do - local metal_raw = df.global.world.raws.inorganics[mat_index] + local metal_raw = df.global.world.raws.inorganics.all[mat_index] str = ('%s%s, '):format(str, string.lower(metal_raw.id)) end diff --git a/makeown.lua b/makeown.lua index a0cbae4a71..244c83a93c 100644 --- a/makeown.lua +++ b/makeown.lua @@ -69,11 +69,11 @@ function clear_enemy_status(unit) status_cache.slot_used[status_slot] = false for index in ipairs(status_cache.rel_map[status_slot]) do - status_cache.rel_map[status_slot][index] = -1 + status_cache.rel_map[status_slot][index].ur = -1 end for index in ipairs(status_cache.rel_map) do - status_cache.rel_map[index][status_slot] = -1 + status_cache.rel_map[index][status_slot].ur = -1 end -- TODO: what if there were status slots taken above status_slot? diff --git a/modtools/add-syndrome.lua b/modtools/add-syndrome.lua index 198193ae3c..5cbd330467 100644 --- a/modtools/add-syndrome.lua +++ b/modtools/add-syndrome.lua @@ -88,7 +88,7 @@ local syndrome if tonumber(args.syndrome) then syndrome = df.syndrome.find(tonumber(args.syndrome)) else - for _,syn in ipairs(df.global.world.raws.syndromes.all) do + for _,syn in ipairs(df.global.world.raws.mat_table.syndromes.all) do if syn.syn_name == args.syndrome then syndrome = syn break diff --git a/modtools/create-unit.lua b/modtools/create-unit.lua index e612d3d894..4fce4e4f4b 100644 --- a/modtools/create-unit.lua +++ b/modtools/create-unit.lua @@ -903,7 +903,7 @@ function domesticateUnit(unit) unit.animal.population.region_y = -1 unit.animal.population.unk_28 = -1 unit.animal.population.population_idx = -1 - unit.animal.population.depth = -1 + unit.animal.population.layer_depth = -1 -- And make them tame (from Dirst) unit.flags1.tame = true @@ -924,7 +924,7 @@ function wildUnit(unit) end unit.animal.population.unk_28 = -1 unit.animal.population.population_idx = -1 -- Eventually want to make a real population - unit.animal.population.depth = -1 -- Eventually this should be a parameter + unit.animal.population.layer_depth = -1 -- Eventually this should be a parameter unit.animal.leave_countdown = 99999 -- Eventually this should be a parameter unit.flags2.roaming_wilderness_population_source = true unit.flags2.roaming_wilderness_population_source_not_a_map_feature = true diff --git a/modtools/reaction-trigger-transition.lua b/modtools/reaction-trigger-transition.lua index 139c4f85cd..0f139e5eb9 100644 --- a/modtools/reaction-trigger-transition.lua +++ b/modtools/reaction-trigger-transition.lua @@ -34,7 +34,7 @@ for _,reaction in ipairs(df.global.world.raws.reactions.reactions) do if product.mat_index < 0 then return end - local inorganic = df.global.world.raws.inorganics[product.mat_index] + local inorganic = df.global.world.raws.inorganics.all[product.mat_index] local didInorganicName for _,syndrome in ipairs(inorganic.material.syndrome.syndrome) do local workerOnly = true diff --git a/modtools/reaction-trigger.lua b/modtools/reaction-trigger.lua index 5514a01513..813fd3aa7a 100644 --- a/modtools/reaction-trigger.lua +++ b/modtools/reaction-trigger.lua @@ -15,7 +15,7 @@ eventful.onUnload.reactionTrigger = function() end local function findSyndrome(name) - for _,syndrome in ipairs(df.global.world.raws.syndromes.all) do + for _,syndrome in ipairs(df.global.world.raws.mat_table.syndromes.all) do if syndrome.syn_name == name then return syndrome end diff --git a/modtools/syndrome-trigger.lua b/modtools/syndrome-trigger.lua index 861848ac77..1718668337 100644 --- a/modtools/syndrome-trigger.lua +++ b/modtools/syndrome-trigger.lua @@ -133,7 +133,7 @@ function processSyndrome(syndrome) end local synFound = false -for _,syn in ipairs(df.global.world.raws.syndromes.all) do +for _,syn in ipairs(df.global.world.raws.mat_table.syndromes.all) do local matchedSyn = false if args.syndrome then if syn.syn_name == args.syndrome then diff --git a/pref-adjust.lua b/pref-adjust.lua index 22b3b3b70d..48a317f2f6 100644 --- a/pref-adjust.lua +++ b/pref-adjust.lua @@ -158,7 +158,7 @@ end function build_all_lists(printflag) list_of_inorganics={} -- Type 0 "Likes iron.." list_of_inorganics_string="" - vec=df.global.world.raws.inorganics -- also df.global.world.raws.inorganics_subset[0].id available + vec=df.global.world.raws.inorganics.all -- also df.global.world.raws.inorganics.all_subset[0].id available for k=0,#vec-1 do name=vec[k].id list_of_inorganics[name]=k diff --git a/region-pops.lua b/region-pops.lua index 80f73ceaab..15d249604c 100644 --- a/region-pops.lua +++ b/region-pops.lua @@ -70,7 +70,7 @@ function enum_populations() end end - for i,v in ipairs(df.global.world.populations) do + for i,v in ipairs(df.global.world.populations.all) do local typeid = df.world_population_type[v.type] local is_plant = is_plant_map[typeid] diff --git a/starvingdead.lua b/starvingdead.lua index f1519c8c0f..42742d5a28 100644 --- a/starvingdead.lua +++ b/starvingdead.lua @@ -65,7 +65,7 @@ function StarvingDead:checkDecay() attribute.value = math.floor(attribute.value - (attribute.value * self.attribute_decay)) end - if unit.curse.time_on_site > (self.death_threshold * 33600) then + if unit.curse.interaction.time_on_site > (self.death_threshold * 33600) then unit.animal.vanish_countdown = 1 end end diff --git a/workorder.lua b/workorder.lua index 32837e7560..21a3d7bde2 100644 --- a/workorder.lua +++ b/workorder.lua @@ -342,7 +342,7 @@ function create_orders(orders, quiet) if it2["bearing"] then local bearing = it2["bearing"] local idx - for i, raw in ipairs(world.raws.inorganics) do + for i, raw in ipairs(world.raws.inorganics.all) do if raw.id == bearing then idx = i break From bc2ec10bed3055921def30904457b9b48d0c66c7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 9 Feb 2025 17:13:28 -0800 Subject: [PATCH 419/811] fix comment --- pref-adjust.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pref-adjust.lua b/pref-adjust.lua index 48a317f2f6..5881028ffb 100644 --- a/pref-adjust.lua +++ b/pref-adjust.lua @@ -158,7 +158,7 @@ end function build_all_lists(printflag) list_of_inorganics={} -- Type 0 "Likes iron.." list_of_inorganics_string="" - vec=df.global.world.raws.inorganics.all -- also df.global.world.raws.inorganics.all_subset[0].id available + vec=df.global.world.raws.inorganics.all -- also df.global.world.raws.inorganics.cheap[0].id available for k=0,#vec-1 do name=vec[k].id list_of_inorganics[name]=k From a822020471aa0647636b259db0f289137d029ca3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 01:29:53 -0800 Subject: [PATCH 420/811] show a description of the order you are about to delete --- changelog.txt | 1 + internal/confirm/specs.lua | 52 +++++++++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 4e53f12d9b..15df963806 100644 --- a/changelog.txt +++ b/changelog.txt @@ -40,6 +40,7 @@ Template for new versions: - `hide-tutorials`: if enabled, also hide tutorial popups for adventure mode - `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game - `gui/notify`: moody dwarf notification turns red when they can't find workshop or items +- `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete ## Removed diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 67628222aa..056dd84172 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -432,10 +432,60 @@ ConfirmSpec{ end, } +local function make_order_desc(order, noun) + local desc = '' + if order.mat_type >= 0 then + local matinfo = dfhack.matinfo.decode(order.mat_type, order.mat_index) + if matinfo then + desc = desc .. ' ' .. matinfo:toString() + end + else + for k,v in pairs(order.material_category) do + if v then + desc = desc .. ' ' .. k + break + end + end + end + return desc .. ' ' .. noun +end + +local orders = df.global.world.manager_orders.all +local itemdefs = df.global.world.raws.itemdefs +local reactions = df.global.world.raws.reactions.reactions ConfirmSpec{ id='order-remove', title='Remove manger order', - message='Are you sure you want to remove this manager order?', + message=function() + local order_desc = '' + local scroll_pos = mi.info.work_orders.scroll_position_work_orders + local y_offset = dfhack.screen.getWindowSize() > 154 and 8 or 10 + local _, y = dfhack.screen.getMousePos() + if y then + local order_idx = scroll_pos + (y - y_offset) // 3 + local order = orders[order_idx] + if order.job_type == df.job_type.CustomReaction then + for _, reaction in ipairs(reactions) do + if reaction.code == order.reaction_name then + order_desc = reaction.name + end + end + elseif order.job_type == df.job_type.MakeArmor then + order_desc = make_order_desc(order, itemdefs.armor[order.item_subtype].name) + elseif order.job_type == df.job_type.MakeWeapon then + order_desc = make_order_desc(order, itemdefs.weapons[order.item_subtype].name) + elseif order.job_type == df.job_type.MakePants then + order_desc = make_order_desc(order, itemdefs.pants[order.item_subtype].name) + elseif order.job_type == df.job_type.SmeltOre then + order_desc = make_order_desc(order, 'ore') + elseif order.job_type == df.job_type.MakeTool then + order_desc = make_order_desc(order, itemdefs.tools[order.item_subtype].name) + else + order_desc = make_order_desc(order, df.job_type.attrs[order.job_type].caption) + end + end + return ('Are you sure you want to remove this manager order?\n\n%s'):format(dfhack.capitalizeStringWords(order_desc)) + end, intercept_keys='_MOUSE_L', context='dwarfmode/Info/WORK_ORDERS/Default', predicate=function() return mi.current_hover == df.main_hover_instruction.WORK_ORDERS_REMOVE end, From 273494caa52ddff73f8eeaed3c037166c70796ca Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 08:08:18 -0800 Subject: [PATCH 421/811] changelog editing pass --- changelog.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 15df963806..d2c106622e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,19 +27,19 @@ Template for new versions: # Future ## New Tools -- `devel/export-map`: Export map tile data to a JSON file. +- `devel/export-map`: export map tile data to a JSON file - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk - `gui/spectate`: interactive UI for configuring `spectate` ## New Features -- `advtools`: new overlay ``advtools.fastcombat``; allows you to skip combat animations and the announcement "More" button +- `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys ## Fixes ## Misc Improvements -- `hide-tutorials`: if enabled, also hide tutorial popups for adventure mode -- `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game -- `gui/notify`: moody dwarf notification turns red when they can't find workshop or items +- `hide-tutorials`: handle tutorial popups for adventure mode +- `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game (in case you hid them all and now want them back) +- `gui/notify`: moody dwarf notification turns red when they can't reach workshop or items - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete ## Removed From 7ef456da88b2c1c20afa0f938ea9332d09b4d464 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 10 Feb 2025 08:44:36 -0800 Subject: [PATCH 422/811] more dt ini export changes --- devel/export-dt-ini.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devel/export-dt-ini.lua b/devel/export-dt-ini.lua index cc58277e54..ad4d5bfbde 100644 --- a/devel/export-dt-ini.lua +++ b/devel/export-dt-ini.lua @@ -116,7 +116,7 @@ address('colors_vector',globals,'world','raws','descriptors','colors') address('shapes_vector',globals,'world','raws','descriptors','shapes') address('reactions_vector',globals,'world','raws','reactions') address('base_materials',globals,'world','raws','mat_table','builtin') -address('all_syndromes_vector',globals,'world','raws','syndromes','all') +address('all_syndromes_vector',globals,'world','raws','mat_table','syndromes','all') address('events_vector',globals,'world','history','events') address('historical_figures_vector',globals,'world','history','figures') address('world_site_type',df.world_site,'type') @@ -336,7 +336,7 @@ address('counters3',df.unit, 'counters2','paralysis') address('limb_counters',df.unit,'status2','limbs_stand_max') address('blood',df.unit,'body','blood_max') address('body_component_info',df.unit,'body','components') -address('layer_status_vector',df.body_component_info,'layer_status') +address('layer_status_vector',df.unit.T_body.T_components,'layer_status') address('wounds_vector',df.unit,'body','wounds') address('mood_skill',df.unit,'job','mood_skill') address('used_items_vector',df.unit,'used_items') From c68a310da8424bad015fc17106d9a1f4996bdba0 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 11 Feb 2025 00:10:18 -0800 Subject: [PATCH 423/811] Update position.lua - Use guidm cursor fn --- position.lua | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/position.lua b/position.lua index 49cf1f3526..094211fb29 100644 --- a/position.lua +++ b/position.lua @@ -2,6 +2,7 @@ --@ module = true local argparse = require('argparse') +local guidm = require('gui.dwarfmode') local function parse_args(args) local opts = {} @@ -17,18 +18,6 @@ local function parse_args(args) return opts end -function get_active_cursor() --Return active fort/adv cursor or nil - if dfhack.world.isAdventureMode() then - local look = df.global.game.main_interface.adventure.look - if look.open and look.cursor:isValid() then - return look.cursor --Note: This is a df.coord - end - elseif df.global.cursor.x >= 0 then - return df.global.cursor - end - return nil --Not active -end - local months = { 'Granite, in early Spring.', @@ -129,7 +118,7 @@ if dfhack_flags.module then end function main(opts) - local cursor = get_active_cursor() + local cursor = guidm.getCursorPos() if opts.copy then --Copy keyboard cursor to clipboard if not cursor then From 8555938578d005215afd14c4024ce8a42946ffcc Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 11 Feb 2025 00:15:21 -0800 Subject: [PATCH 424/811] Update changelog.txt --- changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index f5359e79b1..4965da0d76 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,12 +35,14 @@ Template for new versions: - `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys ## Fixes +- `position`: support for adv mode look cursor ## Misc Improvements - `hide-tutorials`: handle tutorial popups for adventure mode - `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game (in case you hid them all and now want them back) - `gui/notify`: moody dwarf notification turns red when they can't reach workshop or items - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete +- `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. ## Removed @@ -49,7 +51,6 @@ Template for new versions: ## Fixes - `advtools`: fix dfhack-added conversation options not appearing in the ask whereabouts conversation tree - `gui/rename`: fix error when changing the language of a unit's name -- `position`: support for adv mode look cursor ## Misc Improvements - `assign-preferences`: new ``--show`` option to display the preferences of the selected unit From 1bc3501b5ee48ab70b24f96947314c4c3f4a89e7 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Tue, 11 Feb 2025 15:38:17 -0600 Subject: [PATCH 425/811] Update hello-world.lua --- devel/hello-world.lua | 4 ---- 1 file changed, 4 deletions(-) diff --git a/devel/hello-world.lua b/devel/hello-world.lua index 12b9eab7c3..2af576f815 100644 --- a/devel/hello-world.lua +++ b/devel/hello-world.lua @@ -51,14 +51,10 @@ function HelloWorldWindow:init() view_id='level', frame={l=0, w=20}, label='Level:', - label_below=false, key_back='CUSTOM_SHIFT_C', key='CUSTOM_SHIFT_V', options=LEVEL_OPTIONS, initial_option=LEVEL_OPTIONS[1].value, - on_change=function(val) - self.callback{Slider.on_change(val)} - end, }, widgets.Slider{ frame={l=1}, From 2d6d854cb295f1eb16533a1528a31d28c42b7dae Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Tue, 11 Feb 2025 15:40:04 -0600 Subject: [PATCH 426/811] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 4965da0d76..31788f2d80 100644 --- a/changelog.txt +++ b/changelog.txt @@ -43,6 +43,7 @@ Template for new versions: - `gui/notify`: moody dwarf notification turns red when they can't reach workshop or items - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete - `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. +- `devel/hello-world`: updated to show off the new Slider widget ## Removed From d8e6665529e41b143fb65aa7ba1fdaa6645275c2 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Wed, 12 Feb 2025 05:53:48 -0800 Subject: [PATCH 427/811] Use guidm cursor fns * colonies.lua - guidm cursor * devel/light.lua (unavailable) - guidm cursor * devel/query.lua - guidm cursor; same_xyz * devel/tree-info.lua - guidm cursor * gui/advfort.lua (unavailable) - guidm cursor; same_xyz * gui/blueprint.lua - guidm cursor * gui/companion-order.lua (unavailable) - guidm cursor; same_xyz * gui/create-item.lua - add pos arg * gui/tiletypes.lua - guidm cursor * modtools/create-item.lua - respect opts.pos, handle inside hackWish * hfs-pit.lua - guidm cursor; fix up tiletypes * launch.lua - guidm cursor; fix projectile flag * putontable.lua - guidm cursor; same_xyz * source.lua - guidm cursor * stripcaged.lua - guidm cursor * teleport.lua - guidm cursor * toggle-kbd-cursor.lua - use clearCursorPos; exclude adv mode * docs/extinguish.rst - adv tag * docs/firestarter.rst - adv tag * docs/launch.rst - reinstated * docs/putontable.rst - reinstated * docs/toggle-kbd-cursor.rst - fort tag * docs/gui/create-item.rst - add pos arg * docs/modtools/create-item.rst - suggest "here" for pos * Update changelog.txt --- changelog.txt | 9 ++++++- colonies.lua | 6 +++-- devel/light.lua | 8 +----- devel/query.lua | 16 +++++------- devel/tree-info.lua | 7 ++++- docs/extinguish.rst | 2 +- docs/firestarter.rst | 2 +- docs/gui/create-item.rst | 4 +++ docs/launch.rst | 2 +- docs/modtools/create-item.rst | 3 ++- docs/putontable.rst | 2 +- docs/toggle-kbd-cursor.rst | 9 +++++-- gui/advfort.lua | 48 ++++++++++++++++++----------------- gui/blueprint.lua | 2 +- gui/companion-order.lua | 17 +++++-------- gui/create-item.lua | 9 ++++++- gui/tiletypes.lua | 10 ++++---- hfs-pit.lua | 40 ++++++++++++++--------------- launch.lua | 16 ++++++------ modtools/create-item.lua | 34 ++++++++++++++++--------- putontable.lua | 8 +++--- source.lua | 7 ++--- stripcaged.lua | 5 ++-- teleport.lua | 8 +++--- toggle-kbd-cursor.lua | 7 +++-- 25 files changed, 160 insertions(+), 121 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4965da0d76..38d56e424d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,12 +30,18 @@ Template for new versions: - `devel/export-map`: export map tile data to a JSON file - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk - `gui/spectate`: interactive UI for configuring `spectate` +- `launch`: (reinstated) thrash your enemies with a flying suplex +- `putontable`: (reinstated) make an item appear on a table like in adventure mode ## New Features -- `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys +- `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys ## Fixes - `position`: support for adv mode look cursor +- `devel/query`, `devel/tree-info`, `hfs-pit`, `colonies`: now function in adventure mode +- `hfs-pit`: fix up tiletypes of pit walls, better placement of stairs (w/r/t eerie pits and ramp tops) +- `modtools/create-item`: ``hackWish`` now respects ``opts.pos`` and will spawn items there if provided +- `toggle-kbd-cursor`: exclude adventure mode because it isn't compatible ## Misc Improvements - `hide-tutorials`: handle tutorial popups for adventure mode @@ -43,6 +49,7 @@ Template for new versions: - `gui/notify`: moody dwarf notification turns red when they can't reach workshop or items - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete - `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. +- `gui/create-item`: now accepts a ``pos`` argument of where to spawn items ## Removed diff --git a/colonies.lua b/colonies.lua index 554795229e..48b8094d83 100644 --- a/colonies.lua +++ b/colonies.lua @@ -20,6 +20,8 @@ and ``colonies convert TERMITE`` ends your beekeeping industry. ]====] +local guidm = require('gui.dwarfmode') + function findVermin(target_verm) for k,v in ipairs(df.global.world.raws.creatures.all) do if v.creature_id == target_verm then @@ -50,8 +52,8 @@ function convert_vermin_to(target_verm) end function place_vermin(target_verm) - local pos = copyall(df.global.cursor) - if pos.x == -30000 then + local pos = guidm.getCursorPos() + if not pos then qerror("Cursor must be pointing somewhere") end local verm = df.vermin:new() diff --git a/devel/light.lua b/devel/light.lua index aa741f4d07..3786a26976 100644 --- a/devel/light.lua +++ b/devel/light.lua @@ -27,12 +27,6 @@ function setCell(x,y,cell) cell.bo=cell.bo or {r=0,g=0,b=0} render.setCell(x,y,cell) end -function getCursorPos() - local g_cursor=df.global.cursor - if g_cursor.x ~= -30000 then - return copyall(g_cursor) - end -end --luacheck: skip function falloff(color,sqDist,maxdist) local v1=1/(sqDist/maxdist+1) @@ -273,7 +267,7 @@ function LightOverlay:calculateLightSun() end end function LightOverlay:calculateLightCursor() - local c=getCursorPos() + local c=guidm.getCursorPos() if c then diff --git a/devel/query.lua b/devel/query.lua index 18859eaaed..610003873c 100644 --- a/devel/query.lua +++ b/devel/query.lua @@ -2,7 +2,8 @@ -- Written by Josh Cooper(cppcooper) on 2017-12-21, last modified: 2021-06-13 -- Version: 3.2 --luacheck:skip-entirely -local utils=require('utils') +local guidm = require('gui.dwarfmode') +local utils = require('utils') local validArgs = utils.invert({ 'help', @@ -221,14 +222,11 @@ function getSelectionData() elseif args.job then debugf(0,"job selection") selection = dfhack.gui.getSelectedJob(true) - if selection == nil and df.global.cursor.x >= 0 then - local pos = { x=df.global.cursor.x, - y=df.global.cursor.y, - z=df.global.cursor.z } + local pos = guidm.getCursorPos() + if selection == nil and pos then print("searching for a job at the cursor") for _link, job in utils.listpairs(df.global.world.jobs.list) do - local jp = job.pos - if jp.x == pos.x and jp.y == pos.y and jp.z == pos.z then + if same_xyz(job.pos, pos) then if selection == nil then selection = {} end @@ -240,7 +238,7 @@ function getSelectionData() path_info_pattern = path_info elseif args.tile then debugf(0,"tile selection") - local pos = copyall(df.global.cursor) + local pos = guidm.getCursorPos() selection = dfhack.maps.ensureTileBlock(pos.x,pos.y,pos.z) bpos = selection.map_pos path_info = string.format("tile[%d][%d][%d]",pos.x,pos.y,pos.z) @@ -249,7 +247,7 @@ function getSelectionData() tiley = pos.y%16 elseif args.block then debugf(0,"block selection") - local pos = copyall(df.global.cursor) + local pos = guidm.getCursorPos() selection = dfhack.maps.ensureTileBlock(pos.x,pos.y,pos.z) bpos = selection.map_pos path_info = string.format("blocks[%d][%d][%d]",bpos.x,bpos.y,bpos.z) diff --git a/devel/tree-info.lua b/devel/tree-info.lua index 7bf4b42e41..d4d23b0c55 100644 --- a/devel/tree-info.lua +++ b/devel/tree-info.lua @@ -1,5 +1,6 @@ --Print a tree_info visualization of the tree at the cursor. --@module = true +local guidm = require('gui.dwarfmode') -- [w][n][e][s] local branch_chars = { @@ -172,7 +173,11 @@ function printTree(t) end if not dfhack_flags.module then - local p = dfhack.maps.getPlantAtTile(copyall(df.global.cursor)) + local p = guidm.getCursorPos() + if not p then + qerror('No cursor!') + end + p = dfhack.maps.getPlantAtTile(p) if p and p.tree_info then printTree(p.tree_info) else diff --git a/docs/extinguish.rst b/docs/extinguish.rst index ceaae431d2..67ba8c7797 100644 --- a/docs/extinguish.rst +++ b/docs/extinguish.rst @@ -3,7 +3,7 @@ extinguish .. dfhack-tool:: :summary: Put out fires. - :tags: fort armok buildings items map units + :tags: fort adventure armok buildings items map units With this tool, you can put out fires affecting map tiles, plants, units, items, and buildings. diff --git a/docs/firestarter.rst b/docs/firestarter.rst index 5e610d2e92..08e16e0626 100644 --- a/docs/firestarter.rst +++ b/docs/firestarter.rst @@ -3,7 +3,7 @@ firestarter .. dfhack-tool:: :summary: Lights things on fire. - :tags: fort armok items map units + :tags: fort adventure armok items map units Feel the need to burn something? Set items, locations, or even entire inventories on fire! Use while viewing an item, with the cursor over a map tile, diff --git a/docs/gui/create-item.rst b/docs/gui/create-item.rst index ef501afa65..580dec5f1d 100644 --- a/docs/gui/create-item.rst +++ b/docs/gui/create-item.rst @@ -42,6 +42,10 @@ Options ``-f``, ``--unrestricted`` Don't restrict the material options to only those that are normally appropriate for the selected item type. +``-p``, ``--pos ,,`` + If specified, items will be spawned at the given coordinates instead of at + the creator unit's feet. ``here`` can be used in place of ``,,`` + to use the active keyboard cursor. ``--startup`` Instead of showing the item creation interface, start monitoring for a modded reaction with a code of ``DFHACK_WISH``. When a reaction with that diff --git a/docs/launch.rst b/docs/launch.rst index 6b884fcfb7..9f3648b88f 100644 --- a/docs/launch.rst +++ b/docs/launch.rst @@ -3,7 +3,7 @@ launch .. dfhack-tool:: :summary: Thrash your enemies with a flying suplex. - :tags: unavailable + :tags: adventure armok units Attack another unit and then run this command to grab them and fly in a glorious parabolic arc to where you have placed the cursor. You'll land safely and your diff --git a/docs/modtools/create-item.rst b/docs/modtools/create-item.rst index fe96abdd47..a7fbd6ef65 100644 --- a/docs/modtools/create-item.rst +++ b/docs/modtools/create-item.rst @@ -48,4 +48,5 @@ Options caste associated with it. ``-p``, ``--pos ,,`` If specified, items will be spawned at the given coordinates instead of at - the creator unit's feet. + the creator unit's feet. ``here`` can be used in place of ``,,`` + to use the active keyboard cursor. diff --git a/docs/putontable.rst b/docs/putontable.rst index e880ac896d..10d94705a6 100644 --- a/docs/putontable.rst +++ b/docs/putontable.rst @@ -3,7 +3,7 @@ putontable .. dfhack-tool:: :summary: Make an item appear on a table. - :tags: unavailable + :tags: adventure fort armok buildings items To use this tool, move an item to the ground on the same tile as a built table. Then, place the cursor over the table and item and run this command. The item diff --git a/docs/toggle-kbd-cursor.rst b/docs/toggle-kbd-cursor.rst index 80b0e08e62..aba577aceb 100644 --- a/docs/toggle-kbd-cursor.rst +++ b/docs/toggle-kbd-cursor.rst @@ -3,9 +3,14 @@ toggle-kbd-cursor .. dfhack-tool:: :summary: Toggles the keyboard cursor. - :tags: interface + :tags: fort interface -This tool simply toggles the keyboard cursor so you can quickly switch it on when you need it. Many other tools, like `autodump`, need a keyboard cursor for selecting a target tile. Note that you'll still need to enter an interface mode where the keyboard cursor is visible, like mining mode or dumping mode, in order to use the cursor. +This tool simply toggles the keyboard cursor so you can quickly switch it on +when you need it. Many other tools, like `autodump`, need a keyboard cursor for +selecting a target tile. Note that you'll still need to enter an interface mode +where the keyboard cursor is visible, like mining mode or dumping mode, in +order to use the cursor. This tool does not function in adventure mode because +the adventurer cursor cannot be trivially toggled. Usage ----- diff --git a/gui/advfort.lua b/gui/advfort.lua index f63ad98173..1ce1e3f5c6 100644 --- a/gui/advfort.lua +++ b/gui/advfort.lua @@ -69,14 +69,15 @@ build_filter.HUMANish={ --[[ FIXME: maybe let player select which to disable?]] for k,v in ipairs(df.global.plotinfo.economic_stone) do df.global.plotinfo.economic_stone[k]=0 end -local gui = require 'gui' -local wid=require 'gui.widgets' -local dialog=require 'gui.dialogs' -local buildings=require 'dfhack.buildings' -local bdialog=require 'gui.buildings' -local workshopJobs=require 'dfhack.workshops' -local utils=require 'utils' -local gscript=require 'gui.script' +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local wid = require('gui.widgets') +local dialog = require('gui.dialogs') +local buildings = require('dfhack.buildings') +local bdialog = require('gui.buildings') +local workshopJobs = require('dfhack.workshops') +local utils = require('utils') +local gscript = require('gui.script') local advfort_items = reqscript('internal/advfort/advfort_items') @@ -375,7 +376,7 @@ end function AssignUnitToJob(job,unit,unit_pos) job.general_refs:insert("#",{new=df.general_ref_unit_workerst,unit_id=unit.id}) unit.job.current_job=job - unit_pos=unit_pos or {x=job.pos.x,y=job.pos.y,z=job.pos.z} + unit_pos=unit_pos or copyall(job.pos) unit.path.dest:assign(unit_pos) return true end @@ -383,7 +384,7 @@ function SetCreatureRef(args) local job=args.job local pos=args.pos for k,v in pairs(df.global.world.units.active) do - if v.pos.x==pos.x and v.pos.y==pos.y and v.pos.z==pos.z then + if same_xyz(v.pos, pos) then job.general_refs:insert("#",{new=df.general_ref_unit_cageest,unit_id=v.id}) return end @@ -393,7 +394,7 @@ end function SetWebRef(args) local pos=args.pos for k,v in pairs(df.global.world.items.other.ANY_WEBS) do - if v.pos.x==pos.x and v.pos.y==pos.y and v.pos.z==pos.z then + if same_xyz(v.pos, pos) then args.job.general_refs:insert("#",{new=df.general_ref_item,item_id=v.id}) return end @@ -403,7 +404,7 @@ function SetPatientRef(args) local job=args.job local pos=args.pos for k,v in pairs(df.global.world.units.active) do - if v.pos.x==pos.x and v.pos.y==pos.y and v.pos.z==pos.z then + if same_xyz(v.pos, pos) then job.general_refs:insert("#",{new=df.general_ref_unit_patientst,unit_id=v.id}) return end @@ -481,7 +482,7 @@ end function SameSquare(args) local pos1=args.pos local pos2=args.from_pos - if pos1.x==pos2.x and pos1.y==pos2.y and pos1.z==pos2.z then + if same_xyz(pos1, pos2) then return true else return false, "Can only do it on same square" @@ -547,7 +548,7 @@ end function IsUnit(args) local pos=args.pos for k,v in pairs(df.global.world.units.active) do - if v.pos.x==pos.x and v.pos.y==pos.y and v.pos.z==pos.z then + if same_xyz(v.pos, pos) then return true end end @@ -556,7 +557,7 @@ end function itemsAtPos(pos,tbl) local ret=tbl or {} for k,v in pairs(df.global.world.items.other.IN_PLAY) do - if v.pos.x==pos.x and v.pos.y==pos.y and v.pos.z==pos.z and v.flags.on_ground then + if v.flags.on_ground and same_xyz(v.pos, pos) then table.insert(ret,v) end end @@ -776,7 +777,7 @@ function EnumItems(args) end elseif args.pos~=nil then for k,v in pairs(df.global.world.items.other.IN_PLAY) do - if v.pos.x==args.pos.x and v.pos.y==args.pos.y and v.pos.z==args.pos.z and v.flags.on_ground then + if v.flags.on_ground and same_xyz(v.pos, args.pos) then AddItem(ret,v,args.deep) end end @@ -1303,7 +1304,7 @@ function siegeWeaponActionChosen(args,actionid) end args.job_type=action args.unit=dfhack.world.getAdventurer() - local from_pos={x=args.unit.pos.x,y=args.unit.pos.y, z=args.unit.pos.z} + local from_pos=copyall(args.unit.pos) args.from_pos=from_pos args.pos=from_pos elseif actionid==3 then --Fire @@ -1313,7 +1314,7 @@ function siegeWeaponActionChosen(args,actionid) end args.job_type=action args.unit=dfhack.world.getAdventurer() - local from_pos={x=args.unit.pos.x,y=args.unit.pos.y, z=args.unit.pos.z} + local from_pos=copyall(args.unit.pos) args.from_pos=from_pos args.pos=from_pos end @@ -1397,7 +1398,7 @@ function usetool:openShopWindow(building) local filter_pile=workshopJobs.getJobs(building:getType(),building:getSubtype(),building:getCustomType()) if filter_pile then - local state={unit=adv,from_pos={x=adv.pos.x,y=adv.pos.y, z=adv.pos.z},building=building,screen=self,bld=building} + local state={unit=adv,from_pos=copyall(adv.pos),building=building,screen=self,bld=building} local choices={} for k,v in pairs(filter_pile) do table.insert(choices,{job_id=0,text=v.name:lower(),filter=v}) @@ -1687,15 +1688,16 @@ function usetool:fieldInput(keys) unit=adv, pos=moddedpos(adv.pos,MOVEMENT_KEYS[code]), dir=MOVEMENT_KEYS[code], - from_pos={x=adv.pos.x,y=adv.pos.y, z=adv.pos.z}, + from_pos=copyall(adv.pos), post_actions=cur_mode[4], pre_actions=cur_mode[5], job_type=cur_mode[2], screen=self} if code=="SELECT" then --do job in the distance, TODO: check if you can still cheat-mine (and co.) remotely - if df.global.cursor.x~=-30000 then - state.pos={x=df.global.cursor.x,y=df.global.cursor.y,z=df.global.cursor.z} + local cursor=guidm.getCursorPos() + if cursor then + state.pos=cursor else break end @@ -1747,7 +1749,7 @@ function usetool:onInput(keys) local adv=dfhack.world.getAdventurer() if keys.LEAVESCREEN then - if df.global.cursor.x~=-30000 then --if not poiting at anything + if guidm.getCursorPos() then --if not poiting at anything self:sendInputToParent("LEAVESCREEN") --leave poiting else self:dismiss() --leave the adv-tools all together diff --git a/gui/blueprint.lua b/gui/blueprint.lua index abedc26654..2df37f0106 100644 --- a/gui/blueprint.lua +++ b/gui/blueprint.lua @@ -381,7 +381,7 @@ function Blueprint:onShow() end function Blueprint:save_cursor_pos() - self.saved_cursor = copyall(df.global.cursor) + self.saved_cursor = guidm.getCursorPos() end function Blueprint:is_setting_start_pos() diff --git a/gui/companion-order.lua b/gui/companion-order.lua index 71bc644c22..814b4d9d8a 100644 --- a/gui/companion-order.lua +++ b/gui/companion-order.lua @@ -25,10 +25,11 @@ Can be called with '-c' flag to display "cheating" commands. ]====] local gui = require 'gui' +local guidm = require 'gui.dwarfmode' local dlg = require 'gui.dialogs' local args={...} local is_cheat=(#args>0 and args[1]=="-c") -local cursor=xyz2pos(df.global.cursor.x,df.global.cursor.y,df.global.cursor.z) +local cursor=guidm.getCursorPos() local permited_equips={} permited_equips[df.item_backpackst]="UPPERBODY" @@ -46,7 +47,7 @@ function DoesHaveSubtype(item) return true end function CheckCursor(p) - if p.x==-30000 then + if not p then dlg.showMessage( 'Companion orders', 'You must have a cursor on some tile!', COLOR_LIGHTRED @@ -55,12 +56,6 @@ function CheckCursor(p) end return true end -function getxyz() -- this will return pointers x,y and z coordinates. - local x=df.global.cursor.x - local y=df.global.cursor.y - local z=df.global.cursor.z - return x,y,z -- return the coords -end function EnumBodyEquipable(race_id,caste_id) local caste=dfhack.units.getCasteRaw(race_id,caste_id) @@ -190,7 +185,7 @@ end function GetItemsAtPos(pos) local ret={} for k,v in pairs(df.global.world.items.other.IN_PLAY) do - if v.flags.on_ground and v.pos.x==pos.x and v.pos.y==pos.y and v.pos.z==pos.z then + if v.flags.on_ground and same_xyz(v.pos, pos) then table.insert(ret,v) end end @@ -232,7 +227,7 @@ function move_unit( unit,tx,ty,tz ) --copied from http/commands.lua with minor m unit.idle_area_threshold=0 unit.follow_distance=50 --invalidate old path - unit.path.dest={x=unit.idle_area.x,y=unit.idle_area.y,z=unit.idle_area.z} + unit.path.dest=copyall(unit.idle_area) unit.path.goal=df.unit_path_goal.SeekStation unit.path.path.x:resize(0) unit.path.path.y:resize(0) @@ -394,7 +389,7 @@ end}, return false end adv=dfhack.world.getAdventurer() - item=GetItemsAtPos(df.global.cursor)[1] + item=GetItemsAtPos(cursor)[1] print(item.id) for k,v in pairs(unit_list) do v.riding_item_id=item.id diff --git a/gui/create-item.lua b/gui/create-item.lua index ee224fca57..cf5c12a25d 100644 --- a/gui/create-item.lua +++ b/gui/create-item.lua @@ -232,7 +232,8 @@ local default_accessors = { partlayerok, partlayerID = script.showListPrompt('Wish', 'What creature material should it be?', COLOR_LIGHTGREEN, getCreatureMaterialList(raceId, casteId), 1, true) else - --the offsets here are because indexes in lua are wonky (some start at 0, some start at 1), so we adjust for that, as well as the index offset created by inserting the "generic" option at the start of the body part selection prompt + --the offsets here are because indexes in lua are wonky (some start at 0, some start at 1), so we adjust for that, + --as well as the index offset created by inserting the "generic" option at the start of the body part selection prompt bodypart = bodypart - 2 partlayerok, partlayerID = script.showListPrompt('Wish', 'What tissue layer should it be?', COLOR_LIGHTGREEN, getCreaturePartLayerList(raceId, casteId, bodypart), 1, true) @@ -275,6 +276,12 @@ local positionals = argparse.processArgsGetopt({ ... }, { hasArg = true, handler = function(arg) opts.count = argparse.nonnegativeInt(arg, 'count') end, }, + { + 'p', + 'pos', + hasArg = true, + handler = function(arg) opts.pos = argparse.coords(arg, 'pos') end, + }, }) if positionals[1] == 'help' then opts.help = true end diff --git a/gui/tiletypes.lua b/gui/tiletypes.lua index 193f1ca0bb..513fd9181c 100644 --- a/gui/tiletypes.lua +++ b/gui/tiletypes.lua @@ -767,11 +767,11 @@ function BoxSelection:onInput(keys) end local mousePos = dfhack.gui.getMousePos(true) - local cursorPos = copyall(df.global.cursor) - cursorPos.x = math.max(math.min(cursorPos.x, df.global.world.map.x_count - 1), 0) - cursorPos.y = math.max(math.min(cursorPos.y, df.global.world.map.y_count - 1), 0) - + local cursorPos = guidm.getCursorPos() + if cursorPos and keys.SELECT then + cursorPos.x = math.max(math.min(cursorPos.x, df.global.world.map.x_count - 1), 0) + cursorPos.y = math.max(math.min(cursorPos.y, df.global.world.map.y_count - 1), 0) if self.first_point and not self.last_point then if not self.flat or cursorPos.z == self.first_point.z then self.last_point = cursorPos @@ -834,7 +834,7 @@ function BoxSelection:onRenderFrame(dc, rect) if not box then local selectedPos = dfhack.gui.getMousePos(true) if self.useCursor or not selectedPos then - selectedPos = copyall(df.global.cursor) + selectedPos = guidm.getCursorPos() or xyz2pos(nil) selectedPos.x = math.max(math.min(selectedPos.x, df.global.world.map.x_count - 1), 0) selectedPos.y = math.max(math.min(selectedPos.y, df.global.world.map.y_count - 1), 0) end diff --git a/hfs-pit.lua b/hfs-pit.lua index fb4cf7341b..a89c3e6eb3 100644 --- a/hfs-pit.lua +++ b/hfs-pit.lua @@ -27,6 +27,7 @@ Examples:: ]====] +local guidm = require('gui.dwarfmode') local args={...} if args[1] == '?' or args[1] == 'help' then @@ -34,7 +35,7 @@ if args[1] == '?' or args[1] == 'help' then return end -local pos = copyall(df.global.cursor) +local pos = guidm.getCursorPos() local size = tonumber(args[1]) if size == nil or size < 1 then size = 1 end @@ -49,7 +50,7 @@ for index, feature in ipairs(df.global.world.features.map_features) do end end -if pos.x==-30000 then +if not pos then qerror("Select a location by placing the cursor") end local x = 0 @@ -62,41 +63,40 @@ for x=pos.x-size,pos.x+size,1 do while z <= pos.z do local block = dfhack.maps.ensureTileBlock(x,y,z) if block then - if block.tiletype[x%16][y%16] ~= 335 then + local old_tt = block.tiletype[x%16][y%16] + if not hitAir and old_tt ~= df.tiletype.FeatureWall and old_tt ~= df.tiletype.EeriePit then hitAir = true end - if hitAir == true then + if hitAir then if not hitCeiling then if block.global_feature ~= underworldLayer or z > 10 then hitCeiling = true end if stairs == 1 and x == pos.x and y == pos.y then - if block.tiletype[x%16][y%16] == 32 then + if old_tt == df.tiletype.OpenSpace or old_tt == df.tiletype.RampTop then if z == pos.z then - block.tiletype[x%16][y%16] = 56 + block.tiletype[x%16][y%16] = df.tiletype.StoneStairD else - block.tiletype[x%16][y%16] = 55 + block.tiletype[x%16][y%16] = df.tiletype.StoneStairUD end else - block.tiletype[x%16][y%16] = 57 + block.tiletype[x%16][y%16] = df.tiletype.StoneStairU end end end - if hitCeiling == true then + if hitCeiling then local needsWall = block.designation[x%16][y%16].flow_size > 0 or wallOff == 1 if (x == pos.x-size or x == pos.x+size or y == pos.y-size or y == pos.y+size) and z==pos.z then --Do nothing, this is the lip of the hole - elseif x == pos.x-size and y == pos.y-size then if needsWall == true then block.tiletype[x%16][y%16]=320 end - elseif x == pos.x-size and y == pos.y+size then if needsWall == true then block.tiletype[x%16][y%16]=321 end - elseif x == pos.x+size and y == pos.y+size then if needsWall == true then block.tiletype[x%16][y%16]=322 end - elseif x == pos.x+size and y == pos.y-size then if needsWall == true then block.tiletype[x%16][y%16]=323 end - elseif x == pos.x-size or x == pos.x+size then if needsWall == true then block.tiletype[x%16][y%16]=324 end - elseif y == pos.y-size or y == pos.y+size then if needsWall == true then block.tiletype[x%16][y%16]=325 end + elseif x == pos.x-size and y == pos.y-size then if needsWall then block.tiletype[x%16][y%16]=df.tiletype.StoneWallSmoothRD end + elseif x == pos.x-size and y == pos.y+size then if needsWall then block.tiletype[x%16][y%16]=df.tiletype.StoneWallSmoothRU end + elseif x == pos.x+size and y == pos.y+size then if needsWall then block.tiletype[x%16][y%16]=df.tiletype.StoneWallSmoothLU end + elseif x == pos.x+size and y == pos.y-size then if needsWall then block.tiletype[x%16][y%16]=df.tiletype.StoneWallSmoothLD end + elseif x == pos.x-size or x == pos.x+size then if needsWall then block.tiletype[x%16][y%16]=df.tiletype.StoneWallSmoothUD end + elseif y == pos.y-size or y == pos.y+size then if needsWall then block.tiletype[x%16][y%16]=df.tiletype.StoneWallSmoothLR end elseif stairs == 1 and x == pos.x and y == pos.y then - if z == pos.z then block.tiletype[x%16][y%16]=56 - else block.tiletype[x%16][y%16]=55 end - else block.tiletype[x%16][y%16]=32 + if z == pos.z then block.tiletype[x%16][y%16]=df.tiletype.StoneStairD + else block.tiletype[x%16][y%16]=df.tiletype.StoneStairUD end + else block.tiletype[x%16][y%16]=df.tiletype.OpenSpace end - block.designation[x%16][y%16].hidden = false - --block.designation[x%16][y%16].liquid_type = true -- if true, magma. if false, water. block.designation[x%16][y%16].flow_size = 0 dfhack.maps.enableBlockUpdates(block) block.designation[x%16][y%16].flow_forbid = false diff --git a/launch.lua b/launch.lua index 57cf1ef40d..40aaf26995 100644 --- a/launch.lua +++ b/launch.lua @@ -8,16 +8,16 @@ Activate with a cursor on screen and you will go there rapidly. Attack something first to ride them there. ]====] +local guidm = require('gui.dwarfmode') + function launch(unitSource,unitRider) - local curpos - if df.global.adventure.menu == df.ui_advmode_menu.Look then - curpos = df.global.cursor - elseif df.global.gamemode == df.game_mode.ADVENTURE then - qerror("No [l] cursor located! You would have slammed into the ground and exploded.") - else + if not dfhack.world.isAdventureMode() then qerror("Must be used in adventurer mode or the arena!") end - + local curpos = guidm.getCursorPos() + if not curpos then + qerror("No cursor located! You would have slammed into the ground and exploded.") + end local count=0 local l = df.global.world.projectiles.all @@ -61,7 +61,7 @@ function launch(unitSource,unitRider) proj.flags.high_flying=true --this probably doesn't do anything, let me know if you figure out what it is proj.flags.parabolic=true proj.flags.no_collide=true - proj.flags.unk9=true + proj.flags.no_adv_pause=true proj.speed_x=resultx*10000 proj.speed_y=resulty*10000 proj.speed_z=resultz*15000 --higher z speed makes it easier to reach a target safely diff --git a/modtools/create-item.lua b/modtools/create-item.lua index 95811af968..03a60b352f 100644 --- a/modtools/create-item.lua +++ b/modtools/create-item.lua @@ -78,7 +78,10 @@ local function createCorpsePiece(creator, bodypart, partlayer, creatureID, caste casteID = tonumber(casteID) bodypart = tonumber(bodypart) partlayer = tonumber(partlayer) - -- somewhat similar to the bodypart variable below, a value of -1 here means that the user wants to spawn a whole body part. we set the partlayer to 0 (outermost) because the specific layer isn't important, and we're spawning them all anyway. if it's a generic corpsepiece we ignore it, as it gets added to anyway below (we can't do it below because between here and there there's lines that reference the part layer + -- somewhat similar to the bodypart variable below, a value of -1 here means that the user wants to spawn a whole body part. + -- we set the partlayer to 0 (outermost) because the specific layer isn't important, and we're spawning them all anyway. + -- if it's a generic corpsepiece we ignore it, as it gets added to anyway below (we can't do it below because between here and + -- there there's lines that reference the part layer if partlayer == -1 and not generic then partlayer = 0 wholePart = true @@ -177,7 +180,8 @@ local function createCorpsePiece(creator, bodypart, partlayer, creatureID, caste item.race = creatureID item.normal_race = creatureID item.normal_caste = casteID - -- usually the first two castes are for the creature's sex, so we set the item's sex to the caste if both the creature has one and it's a valid sex id (0 or 1) + -- usually the first two castes are for the creature's sex, so we set the item's sex to + -- the caste if both the creature has one and it's a valid sex id (0 or 1) if casteID < 2 and #(creatorRaceRaw.caste) > 1 then item.sex = casteID else @@ -198,12 +202,14 @@ local function createCorpsePiece(creator, bodypart, partlayer, creatureID, caste for i,n in pairs(creatorBody.body_parts) do -- inserts item.body.body_part_relsize:insert('#', n.relsize) - item.body.components.body_part_status:insert(i, creator.body.components.body_part_status[0]) --copy the status of the creator's first part to every body_part_status of the desired creature + --copy the status of the creator's first part to every body_part_status of the desired creature + item.body.components.body_part_status:insert(i, creator.body.components.body_part_status[0]) item.body.components.body_part_status[i].missing = true end for i in pairs(creatorBody.layer_part) do -- inserts - item.body.components.layer_status:insert(i, creator.body.components.layer_status[0]) --copy the layer status of the creator's first layer to every layer_status of the desired creature + -- copy the layer status of the creator's first layer to every layer_status of the desired creature + item.body.components.layer_status:insert(i, creator.body.components.layer_status[0]) item.body.components.layer_status[i].gone = true end if item_type == 'CORPSE' then @@ -222,7 +228,9 @@ local function createCorpsePiece(creator, bodypart, partlayer, creatureID, caste item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false else -- search through the target creature's body parts and bring back every one which has the desired material - if creatorRaceRaw.tissue[creatorBody.body_parts[i].layers[n].tissue_id].tissue_material_str[1] == layerMat and creatorBody.body_parts[i].token ~= 'SKULL' and not creatorBody.body_parts[i].flags.SMALL then + if creatorRaceRaw.tissue[creatorBody.body_parts[i].layers[n].tissue_id].tissue_material_str[1] == layerMat and + creatorBody.body_parts[i].token ~= 'SKULL' and not creatorBody.body_parts[i].flags.SMALL then + item.body.components.body_part_status[i].missing = false item.body.components.layer_status[creatorBody.body_parts[i].layers[n].layer_id].gone = false -- save the index of the bone layer to a variable @@ -251,7 +259,8 @@ end local function createItem(mat, itemType, quality, creator, description, amount) -- The "reaction-gloves" tweak can cause this to create multiple gloves local items = dfhack.items.createItem(creator, itemType[1], itemType[2], mat[1], mat[2]) - assert(#items > 0, ('failed to create item: item_type: %s, item_subtype: %s, mat_type: %s, mat_index: %s, unit: %s'):format(itemType[1], itemType[2], mat[1], mat[2], creator and creator.id or 'nil')) + assert(#items > 0, ('failed to create item: item_type: %s, item_subtype: %s, mat_type: %s, mat_index: %s, unit: %s'):format( + itemType[1], itemType[2], mat[1], mat[2], creator and creator.id or 'nil')) local item = items[1] local mat_token = dfhack.matinfo.decode(item):getToken() quality = math.max(0, math.min(5, quality - 1)) @@ -350,6 +359,12 @@ function hackWish(accessors, opts) end end end + if opts.pos then + for _,item in ipairs(items) do + dfhack.items.moveToGround(item, opts.pos) + end + end + return items end @@ -435,9 +450,4 @@ local accessors = { end, } -local items = hackWish(accessors, {}) -if items and opts.pos then - for _,item in ipairs(items) do - dfhack.items.moveToGround(item, opts.pos) - end -end +hackWish(accessors, {pos=opts.pos}) diff --git a/putontable.lua b/putontable.lua index 072e75171f..ca8d00e5d7 100644 --- a/putontable.lua +++ b/putontable.lua @@ -10,19 +10,21 @@ Arguments: ]====] -local pos=df.global.cursor +local guidm = require('gui.dwarfmode') + local args={...} local doall if args[1]=="-a" or args[1]=="--all" then doall=true end local items={} --as:df.item[] -local build=dfhack.buildings.findAtTile(pos.x,pos.y,pos.z) +local pos = guidm.getCursorPos() +local build = pos and dfhack.buildings.findAtTile(pos.x,pos.y,pos.z) or nil if not df.building_tablest:is_instance(build) then error("No table found at cursor") end for k,v in pairs(df.global.world.items.other.IN_PLAY) do - if pos.x==v.pos.x and pos.y==v.pos.y and pos.z==v.pos.z and v.flags.on_ground then + if v.flags.on_ground and same_xyz(v.pos, pos) then table.insert(items,v) if not doall then break diff --git a/source.lua b/source.lua index ff9305ea32..67d052f95b 100644 --- a/source.lua +++ b/source.lua @@ -1,4 +1,5 @@ --@ module = true +local guidm = require('gui.dwarfmode') local repeatUtil = require('repeat-util') local GLOBAL_KEY = 'source' -- used for state change hooks and persistence @@ -130,11 +131,11 @@ function main(args) return end - local targetPos = copyall(df.global.cursor) + local targetPos = guidm.getCursorPos() local index = find_liquid_source_at_pos(targetPos) if command == 'delete' then - if targetPos.x < 0 then + if not targetPos then qerror("Please place the cursor where there is a source to delete") end if index then @@ -147,7 +148,7 @@ function main(args) end if command == 'add' then - if targetPos.x < 0 then + if not targetPos then qerror('Please place the cursor where you would like a source') end local liquidArg = args[2] diff --git a/stripcaged.lua b/stripcaged.lua index a334daba44..d153d8cda9 100644 --- a/stripcaged.lua +++ b/stripcaged.lua @@ -1,4 +1,5 @@ local argparse = require('argparse') +local guidm = require('gui.dwarfmode') local opts = {} local positionals = argparse.processArgsGetopt({...}, { {'h', 'help', handler = function() opts.help = true end}, @@ -206,8 +207,8 @@ if positionals[2] == 'here' then end end -- Is the player trying to select a cage using the keyboard cursor? - elseif df.global.cursor.z > -10000 then -- cursor has values around -30000 if not valid/active. - local cursor = df.global.cursor + elseif guidm.getCursorPos() then + local cursor = guidm.getCursorPos() for _, cage in ipairs(df.global.world.items.other.ANY_CAGE_OR_TRAP) do if same_xyz(cursor, cage.pos) then table.insert(list, cage) diff --git a/teleport.lua b/teleport.lua index 4a55f6e99b..108f7e8673 100644 --- a/teleport.lua +++ b/teleport.lua @@ -28,6 +28,8 @@ Examples: ]====] +local guidm = require('gui.dwarfmode') + function teleport(unit,pos) dfhack.units.teleport(unit, pos) end @@ -53,12 +55,12 @@ if args.showunitid or args.showpos then if args.showunitid then print(dfhack.gui.getSelectedUnit(true).id) else - printall(df.global.cursor) + printall(guidm.getCursorPos()) end else local unit = tonumber(args.unit) and df.unit.find(tonumber(args.unit)) or dfhack.gui.getSelectedUnit(true) - local pos = not(not args.x or not args.y or not args.z) and {x=args.x,y=args.y,z=args.z} or {x=df.global.cursor.x,y=df.global.cursor.y,z=df.global.cursor.z} + local pos = not(not args.x or not args.y or not args.z) and {x=args.x,y=args.y,z=args.z} or guidm.getCursorPos() if not unit then qerror('A unit needs to be selected or specified. Use teleport -showunitid to get a unit\'s ID.') end - if not pos.x or pos.x==-30000 then qerror('A position needs to be highlighted or specified. Use teleport -showpos to get a position\'s exact xyz values.') end + if not pos then qerror('A position needs to be highlighted or specified. Use teleport -showpos to get a position\'s exact xyz values.') end teleport(unit,pos) end diff --git a/toggle-kbd-cursor.lua b/toggle-kbd-cursor.lua index ae66b1900d..3a9b2b462b 100644 --- a/toggle-kbd-cursor.lua +++ b/toggle-kbd-cursor.lua @@ -1,10 +1,13 @@ local guidm = require('gui.dwarfmode') -local flags = df.global.d_init.feature.flags +if dfhack.world.isAdventureMode() then + qerror('Adventure mode unsupported!') +end +local flags = df.global.d_init.feature.flags if flags.KEYBOARD_CURSOR then flags.KEYBOARD_CURSOR = false - guidm.setCursorPos(xyz2pos(-30000, -30000, -30000)) + guidm.clearCursorPos() print('Keyboard cursor disabled.') else guidm.setCursorPos(guidm.Viewport.get():getCenter()) From 30af7c95317613884078d2b82e6f3d2d6e5c4881 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Wed, 12 Feb 2025 14:32:52 +0000 Subject: [PATCH 428/811] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- gui/tiletypes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/tiletypes.lua b/gui/tiletypes.lua index 513fd9181c..708359ba57 100644 --- a/gui/tiletypes.lua +++ b/gui/tiletypes.lua @@ -768,7 +768,7 @@ function BoxSelection:onInput(keys) local mousePos = dfhack.gui.getMousePos(true) local cursorPos = guidm.getCursorPos() - + if cursorPos and keys.SELECT then cursorPos.x = math.max(math.min(cursorPos.x, df.global.world.map.x_count - 1), 0) cursorPos.y = math.max(math.min(cursorPos.y, df.global.world.map.y_count - 1), 0) From 971ae8378f5e0ad00f894a0d7d08cc97b32af823 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Wed, 12 Feb 2025 21:24:47 -0800 Subject: [PATCH 429/811] Make toggle-kbd-cursor work with adv mode * Update toggle-kbd-cursor.lua * Update toggle-kbd-cursor.rst * Update changelog.txt --- changelog.txt | 3 +-- docs/toggle-kbd-cursor.rst | 6 +++--- toggle-kbd-cursor.lua | 25 ++++++++++++++----------- 3 files changed, 18 insertions(+), 16 deletions(-) diff --git a/changelog.txt b/changelog.txt index 38d56e424d..cef1f1948a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -38,10 +38,9 @@ Template for new versions: ## Fixes - `position`: support for adv mode look cursor -- `devel/query`, `devel/tree-info`, `hfs-pit`, `colonies`: now function in adventure mode +- `devel/query`, `devel/tree-info`, `hfs-pit`, `colonies`, `toggle-kbd-cursor`: now function in adventure mode - `hfs-pit`: fix up tiletypes of pit walls, better placement of stairs (w/r/t eerie pits and ramp tops) - `modtools/create-item`: ``hackWish`` now respects ``opts.pos`` and will spawn items there if provided -- `toggle-kbd-cursor`: exclude adventure mode because it isn't compatible ## Misc Improvements - `hide-tutorials`: handle tutorial popups for adventure mode diff --git a/docs/toggle-kbd-cursor.rst b/docs/toggle-kbd-cursor.rst index aba577aceb..4ae0e588d7 100644 --- a/docs/toggle-kbd-cursor.rst +++ b/docs/toggle-kbd-cursor.rst @@ -3,14 +3,14 @@ toggle-kbd-cursor .. dfhack-tool:: :summary: Toggles the keyboard cursor. - :tags: fort interface + :tags: adventure fort interface This tool simply toggles the keyboard cursor so you can quickly switch it on when you need it. Many other tools, like `autodump`, need a keyboard cursor for selecting a target tile. Note that you'll still need to enter an interface mode where the keyboard cursor is visible, like mining mode or dumping mode, in -order to use the cursor. This tool does not function in adventure mode because -the adventurer cursor cannot be trivially toggled. +order to use the cursor. In adventure mode, this tool toggles look mode via +simulated input. Usage ----- diff --git a/toggle-kbd-cursor.lua b/toggle-kbd-cursor.lua index 3a9b2b462b..3033049dbe 100644 --- a/toggle-kbd-cursor.lua +++ b/toggle-kbd-cursor.lua @@ -1,16 +1,19 @@ +local gui = require('gui') local guidm = require('gui.dwarfmode') if dfhack.world.isAdventureMode() then - qerror('Adventure mode unsupported!') -end - -local flags = df.global.d_init.feature.flags -if flags.KEYBOARD_CURSOR then - flags.KEYBOARD_CURSOR = false - guidm.clearCursorPos() - print('Keyboard cursor disabled.') + local open = df.global.game.main_interface.adventure.look.open + gui.simulateInput(dfhack.gui.getDFViewscreen(), open and 'LEAVESCREEN' or 'A_LOOK') + print('Look mode '..(open and 'disabled.' or 'enabled.')) else - guidm.setCursorPos(guidm.Viewport.get():getCenter()) - flags.KEYBOARD_CURSOR = true - print('Keyboard cursor enabled.') + local flags = df.global.d_init.feature.flags + if flags.KEYBOARD_CURSOR then + flags.KEYBOARD_CURSOR = false + guidm.clearCursorPos() + print('Keyboard cursor disabled.') + else + guidm.setCursorPos(guidm.Viewport.get():getCenter()) + flags.KEYBOARD_CURSOR = true + print('Keyboard cursor enabled.') + end end From 4af65c03bf6f0028e5f9e03af125bee0266688b0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 14 Feb 2025 20:01:04 -0800 Subject: [PATCH 430/811] update use of unit_preference.active to unit_preference.flags.visible --- internal/caravan/common.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index d0974c91f5..996a616562 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -345,7 +345,7 @@ end local function analyze_noble(unit, risky_items, banned_items) for _, preference in ipairs(unit.status.current_soul.preferences) do if preference.type == df.unitpref_type.LikeItem and - preference.active + preference.flags.visible then register_item_type(risky_items, preference, banned_items) end From e5cb6004c786f05749591f9f8290e716f3183f86 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 16 Feb 2025 13:46:25 -0800 Subject: [PATCH 431/811] adapt to changes in goodflag structure enum -> bitfield --- internal/caravan/trade.lua | 183 +++++++++++++------------------------ internal/confirm/specs.lua | 2 - 2 files changed, 61 insertions(+), 124 deletions(-) diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index d32db99456..f27e949ea5 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -9,6 +9,7 @@ local common = reqscript('internal/caravan/common') local gui = require('gui') local overlay = require('plugins.overlay') local predicates = reqscript('internal/caravan/predicates') +local utils = require('utils') local widgets = require('gui.widgets') trader_selected_state = trader_selected_state or {} @@ -16,15 +17,6 @@ broker_selected_state = broker_selected_state or {} handle_ctrl_click_on_render = handle_ctrl_click_on_render or false handle_shift_click_on_render = handle_shift_click_on_render or false -local GOODFLAG = { - UNCONTAINED_UNSELECTED = 0, - UNCONTAINED_SELECTED = 1, - CONTAINED_UNSELECTED = 2, - CONTAINED_SELECTED = 3, - CONTAINER_COLLAPSED_UNSELECTED = 4, - CONTAINER_COLLAPSED_SELECTED = 5, -} - local trade = df.global.game.main_interface.trade -- ------------------- @@ -39,50 +31,13 @@ Trade.ATTRS { resize_min={w=48, h=40}, } -local TOGGLE_MAP = { - [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_SELECTED, - [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, - [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_SELECTED, - [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_UNSELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, -} - -local TARGET_MAP = { - [true]={ - [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_SELECTED, - [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_SELECTED, - [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_SELECTED, - [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_SELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, - }, - [false]={ - [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, - [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, - [GOODFLAG.CONTAINED_UNSELECTED] = GOODFLAG.CONTAINED_UNSELECTED, - [GOODFLAG.CONTAINED_SELECTED] = GOODFLAG.CONTAINED_UNSELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, - }, -} - -local TARGET_REVMAP = { - [GOODFLAG.UNCONTAINED_UNSELECTED] = false, - [GOODFLAG.UNCONTAINED_SELECTED] = true, - [GOODFLAG.CONTAINED_UNSELECTED] = false, - [GOODFLAG.CONTAINED_SELECTED] = true, - [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = false, - [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = true, -} - local function get_entry_icon(data) - if TARGET_REVMAP[trade.goodflag[data.list_idx][data.item_idx]] then + if trade.goodflag[data.list_idx][data.item_idx].selected then return common.ALL_PEN end end -local function sort_noop(a, b) +local function sort_noop() -- this function is used as a marker and never actually gets called error('sort_noop should not be called') end @@ -375,7 +330,7 @@ function Trade:cache_choices(list_idx, trade_bins) local parent_data for item_idx, item in ipairs(trade.good[list_idx]) do local goodflag = goodflags[item_idx] - if goodflag ~= GOODFLAG.CONTAINED_UNSELECTED and goodflag ~= GOODFLAG.CONTAINED_SELECTED then + if not goodflag.contained then parent_data = nil end local is_banned, is_risky = common.scan_banned(item, self.risky_items) @@ -487,11 +442,13 @@ end local function toggle_item_base(choice, target_value) local goodflag = trade.goodflag[choice.data.list_idx][choice.data.item_idx] - local goodflag_map = target_value == nil and TOGGLE_MAP or TARGET_MAP[target_value] - trade.goodflag[choice.data.list_idx][choice.data.item_idx] = goodflag_map[goodflag] - target_value = TARGET_REVMAP[trade.goodflag[choice.data.list_idx][choice.data.item_idx]] + if target_value == nil then + target_value = not goodflag.selected + end + local prev_value = goodflag.selected + goodflag.selected = target_value if choice.data.update_container_fn then - choice.data.update_container_fn(TARGET_REVMAP[goodflag], target_value) + choice.data.update_container_fn(prev_value, target_value) end return target_value end @@ -591,39 +548,40 @@ local function set_height(list_idx, delta) trade.i_height[list_idx] - page_height)) end -local function select_shift_clicked_container_items(new_state, old_state, list_idx) +local function flags_match(goodflag1, goodflag2) + return goodflag1.selected == goodflag2.selected and + goodflag1.contained == goodflag2.contained and + goodflag1.container_collapsed == goodflag2.container_collapsed and + goodflag1.filtered_off == goodflag2.filtered_off +end + +local function select_shift_clicked_container_items(new_state, old_state_fn, list_idx) -- if ctrl is also held, collapse the container too local also_collapse = dfhack.internal.getModifiers().ctrl - local collapsed_item_count, collapsing_container, in_container = 0, false, false + local collapsed_item_count, collapsing_container, in_target_container = 0, false, false for k, goodflag in ipairs(new_state) do - if in_container then - if goodflag <= GOODFLAG.UNCONTAINED_SELECTED - or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then - break - end - - new_state[k] = GOODFLAG.CONTAINED_SELECTED - + if in_target_container then + if not goodflag.contained then break end + goodflag.selected = true if collapsing_container then collapsed_item_count = collapsed_item_count + 1 end goto continue end - if goodflag == old_state[k] then goto continue end + local old_goodflag = old_state_fn(k) + if flags_match(goodflag, old_goodflag) then goto continue end local is_container = df.item_binst:is_instance(trade.good[list_idx][k]) if not is_container then goto continue end -- deselect the container itself - if also_collapse or - old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED or - old_state[k] == GOODFLAG.CONTAINER_COLLAPSED_SELECTED then - collapsing_container = goodflag == GOODFLAG.UNCONTAINED_SELECTED - new_state[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED - else - new_state[k] = GOODFLAG.UNCONTAINED_UNSELECTED + goodflag.selected = false + + if also_collapse or old_goodflag.container_collapsed then + goodflag.container_collapsed = true + collapsing_container = not old_goodflag.container_collapsed end - in_container = true + in_target_container = true ::continue:: end @@ -633,37 +591,27 @@ local function select_shift_clicked_container_items(new_state, old_state, list_i end end -local CTRL_CLICK_STATE_MAP = { - [GOODFLAG.UNCONTAINED_UNSELECTED] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED, - [GOODFLAG.UNCONTAINED_SELECTED] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED] = GOODFLAG.UNCONTAINED_UNSELECTED, - [GOODFLAG.CONTAINER_COLLAPSED_SELECTED] = GOODFLAG.UNCONTAINED_SELECTED, -} - -- collapses uncollapsed containers and restores the selection state for the container -- and contained items -local function toggle_ctrl_clicked_containers(new_state, old_state, list_idx) - local toggled_item_count, in_container, is_collapsing = 0, false, false +local function toggle_ctrl_clicked_containers(new_state, old_state_fn, list_idx) + local toggled_item_count, in_target_container, is_collapsing = 0, false, false for k, goodflag in ipairs(new_state) do - if in_container then - if goodflag <= GOODFLAG.UNCONTAINED_SELECTED - or goodflag >= GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then - break - end + local old_goodflag = old_state_fn(k) + if in_target_container then + if not goodflag.contained then break end toggled_item_count = toggled_item_count + 1 - new_state[k] = old_state[k] + utils.assign(goodflag, old_goodflag) goto continue end - if goodflag == old_state[k] then goto continue end - local is_contained = goodflag == GOODFLAG.CONTAINED_UNSELECTED or goodflag == GOODFLAG.CONTAINED_SELECTED - if is_contained then goto continue end + if flags_match(goodflag, old_goodflag) or goodflag.contained then goto continue end local is_container = df.item_binst:is_instance(trade.good[list_idx][k]) if not is_container then goto continue end - new_state[k] = CTRL_CLICK_STATE_MAP[old_state[k]] - in_container = true - is_collapsing = goodflag == GOODFLAG.UNCONTAINED_UNSELECTED or goodflag == GOODFLAG.UNCONTAINED_SELECTED + goodflag.selected = old_goodflag.selected + goodflag.container_collapsed = not old_goodflag.container_collapsed + in_target_container = true + is_collapsing = goodflag.container_collapsed ::continue:: end @@ -696,27 +644,17 @@ end local function collapseContainers(item_list, list_idx) local num_items_collapsed = 0 for k, goodflag in ipairs(item_list) do - if goodflag == GOODFLAG.CONTAINED_UNSELECTED - or goodflag == GOODFLAG.CONTAINED_SELECTED then - goto continue - end + if goodflag.contained then goto continue end local item = trade.good[list_idx][k] local is_container = df.item_binst:is_instance(item) if not is_container then goto continue end - local collapsed_this_container = false - if goodflag == GOODFLAG.UNCONTAINED_SELECTED then - item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_SELECTED - collapsed_this_container = true - elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED then - item_list[k] = GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED - collapsed_this_container = true - end - - if collapsed_this_container then + if not goodflag.container_collapsed then + goodflag.container_collapsed = true num_items_collapsed = num_items_collapsed + #dfhack.items.getContainedItems(item) end + ::continue:: end @@ -736,8 +674,14 @@ local function collapseEverything() end local function copyGoodflagState() - trader_selected_state = copyall(trade.goodflag[0]) - broker_selected_state = copyall(trade.goodflag[1]) + -- utils.clone will return a lua table, with indices offset by 1 + -- we'll use getSavedGoodflag to map the index back to the original value + trader_selected_state = utils.clone(trade.goodflag[0], true) + broker_selected_state = utils.clone(trade.goodflag[1], true) +end + +local function getSavedGoodflag(saved_state, k) + return saved_state[k+1] end TradeOverlay = defclass(TradeOverlay, overlay.OverlayWidget) @@ -798,12 +742,12 @@ end function TradeOverlay:onRenderBody(dc) if handle_shift_click_on_render then handle_shift_click_on_render = false - select_shift_clicked_container_items(trade.goodflag[0], trader_selected_state, 0) - select_shift_clicked_container_items(trade.goodflag[1], broker_selected_state, 1) + select_shift_clicked_container_items(trade.goodflag[0], curry(getSavedGoodflag, trader_selected_state), 0) + select_shift_clicked_container_items(trade.goodflag[1], curry(getSavedGoodflag, broker_selected_state), 1) elseif handle_ctrl_click_on_render then handle_ctrl_click_on_render = false - toggle_ctrl_clicked_containers(trade.goodflag[0], trader_selected_state, 0) - toggle_ctrl_clicked_containers(trade.goodflag[1], broker_selected_state, 1) + toggle_ctrl_clicked_containers(trade.goodflag[0], curry(getSavedGoodflag, trader_selected_state), 0) + toggle_ctrl_clicked_containers(trade.goodflag[1], curry(getSavedGoodflag, broker_selected_state), 1) end end @@ -915,12 +859,10 @@ function for_selected_item(list_idx, fn) local in_selected_container = false for item_idx, item in ipairs(trade.good[list_idx]) do local goodflag = goodflags[item_idx] - if goodflag == GOODFLAG.UNCONTAINED_SELECTED or goodflag == GOODFLAG.CONTAINER_COLLAPSED_SELECTED then - in_selected_container = true - elseif goodflag == GOODFLAG.UNCONTAINED_UNSELECTED or goodflag == GOODFLAG.CONTAINER_COLLAPSED_UNSELECTED then - in_selected_container = false + if not goodflag.contained then + in_selected_container = goodflag.selected end - if in_selected_container or TARGET_REVMAP[goodflag] then + if in_selected_container or goodflag.selected then if fn(item_idx, item) then return end @@ -954,10 +896,7 @@ end function Ethics:deselect_transgressions() local goodflags = trade.goodflag[1] for _,choice in ipairs(self.choices) do - local goodflag = goodflags[choice.data.item_idx] - if TARGET_REVMAP[goodflag] then - goodflags[choice.data.item_idx] = TOGGLE_MAP[goodflag] - end + goodflags[choice.data.item_idx].selected = false end self:rescan() end diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 056dd84172..73b9179e41 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -67,7 +67,6 @@ end local function trade_goods_all_selected(which) local num_selected = 0 trade_internal.for_selected_item(which, function(idx) - print(idx) num_selected = num_selected + 1 end) return #mi.trade.goodflag[which] == num_selected @@ -336,7 +335,6 @@ ConfirmSpec{ return uniform_has_changes() end if clicked_on_confirm_button(mouse_offset) then - print('confirm click detected') clear_uniform_record() else ensure_uniform_record() From 8639a8f17e43e0caba48137a407c2ac60abc3d15 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 17 Feb 2025 11:16:30 -0800 Subject: [PATCH 432/811] add changelog entry for gui/notes --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 4965da0d76..1afdc48926 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,7 @@ Template for new versions: - `devel/export-map`: export map tile data to a JSON file - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk - `gui/spectate`: interactive UI for configuring `spectate` +- `gui/notes`: UI for adding and managing notes attached to tiles on the map ## New Features - `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys From 8665a1a743ba635211a5c3d70bd7a182b0c14a1c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 20 Feb 2025 15:47:12 -0800 Subject: [PATCH 433/811] implement shift click to follow --- changelog.txt | 1 + docs/gui/sitemap.rst | 8 ++++--- gui/sitemap.lua | 55 ++++++++++++++++++++++++++++++++++++++------ 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/changelog.txt b/changelog.txt index 1afdc48926..97d2ed0c7a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -44,6 +44,7 @@ Template for new versions: - `gui/notify`: moody dwarf notification turns red when they can't reach workshop or items - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete - `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. +- `gui/sitemap`: shift click to start following the selected unit or artifact ## Removed diff --git a/docs/gui/sitemap.rst b/docs/gui/sitemap.rst index 74e1fde942..ed1d89521d 100644 --- a/docs/gui/sitemap.rst +++ b/docs/gui/sitemap.rst @@ -7,9 +7,11 @@ gui/sitemap This simple UI gives you searchable lists of people, locations (temples, guildhalls, hospitals, taverns, and libraries), and artifacts in the local area. -Clicking on a list item will zoom the map to the target. If you are zooming to -a location and the location has multiple zones attached to it, clicking again -will zoom to each component zone in turn. +Clicking on a list item will zoom the map to the target. In fort mode, +shift-clicking will zoom to the unit or artifact and lock the camera to the +target with follow mode. If you are zooming to a location and the location has +multiple zones attached to it, clicking again will zoom to each component zone +in turn. Locations are attached to a site, so if you're in adventure mode, you must enter a site before searching for locations. For worldgen sites, many locations diff --git a/gui/sitemap.lua b/gui/sitemap.lua index 1f779bfc9c..671212f919 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -13,7 +13,8 @@ Sitemap.ATTRS { frame_title='Sitemap', frame={w=57, r=2, t=18, h=25}, resizable=true, - resize_min={w=43, h=20}, + resize_min={w=44, h=20}, + frame_inset={l=1, t=1, r=0, b=0}, } local function to_title_case(str) @@ -169,6 +170,19 @@ local function zoom_to_unit(_, choice) if not unit then return end dfhack.gui.revealInDwarfmodeMap( xyz2pos(dfhack.units.getPosition(unit)), true, true) + return unit.id +end + +local function follow_unit(idx, choice) + local unit_id = zoom_to_unit(idx, choice) + if not unit_id or not dfhack.world.isFortressMode() then return end + df.global.plotinfo.follow_item = -1 + df.global.plotinfo.follow_unit = unit_id + pcall(function() + -- if spectate is available, add the unit to the follow history + local spectate = require('plugins.spectate') + spectate.spectate_addToHistory(unit_id) + end) end local function get_artifact_choices() @@ -192,6 +206,33 @@ local function zoom_to_item(_, choice) if not item then return end dfhack.gui.revealInDwarfmodeMap( xyz2pos(dfhack.items.getPosition(item)), true, true) + return item.id +end + +local function follow_item(idx, choice) + local item_id = zoom_to_item(idx, choice) + if not item_id or not dfhack.world.isFortressMode() then return end + df.global.plotinfo.follow_item = item_id + df.global.plotinfo.follow_unit = -1 +end + +local function get_bottom_text() + local text = { + 'Click on a name or hit ', {text='Enter', pen=COLOR_LIGHTGREEN}, ' to zoom to', NEWLINE, + 'the selected target.', + } + + if not dfhack.world.isFortressMode() then + table.insert(text, NEWLINE) + table.insert(text, NEWLINE) + return text + end + + table.insert(text, ' Shift-click or') + table.insert(text, NEWLINE) + table.insert(text, {text='Shift-Enter', pen=COLOR_LIGHTGREEN}) + table.insert(text, ' to zoom and follow unit/item.') + return text end function Sitemap:init() @@ -217,7 +258,7 @@ function Sitemap:init() }, widgets.Pages{ view_id='pages', - frame={t=3, l=0, b=5, r=0}, + frame={t=3, l=0, b=6, r=0}, subviews={ widgets.Panel{ subviews={ @@ -230,6 +271,7 @@ function Sitemap:init() widgets.FilteredList{ view_id='list', on_submit=zoom_to_unit, + on_submit2=follow_unit, choices=unit_choices, visible=#unit_choices > 0, }, @@ -255,6 +297,7 @@ function Sitemap:init() widgets.FilteredList{ view_id='list', on_submit=zoom_to_next_zone, + on_submit2=zoom_to_next_zone, choices=location_choices, visible=#location_choices > 0, }, @@ -271,6 +314,7 @@ function Sitemap:init() widgets.FilteredList{ view_id='list', on_submit=zoom_to_item, + on_submit2=follow_item, choices=artifact_choices, visible=#artifact_choices > 0, }, @@ -279,17 +323,14 @@ function Sitemap:init() }, }, widgets.Divider{ - frame={b=3, h=1}, + frame={b=4, h=1, l=0, r=1}, frame_style=gui.FRAME_THIN, frame_style_l=false, frame_style_r=false, }, widgets.Label{ frame={b=0, l=0}, - text={ - 'Click on a name or hit ', {text='Enter', pen=COLOR_LIGHTGREEN}, NEWLINE, - 'to zoom to the selected target.' - }, + text=get_bottom_text(), }, } end From 7abd0f97439203a17d49d8101f446ea5a72cd92d Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Fri, 21 Feb 2025 21:08:03 +0100 Subject: [PATCH 434/811] Delete docs/gui/tooltips.rst --- docs/gui/tooltips.rst | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 docs/gui/tooltips.rst diff --git a/docs/gui/tooltips.rst b/docs/gui/tooltips.rst deleted file mode 100644 index b01d076a26..0000000000 --- a/docs/gui/tooltips.rst +++ /dev/null @@ -1,23 +0,0 @@ -gui/tooltips -============ - -.. dfhack-tool:: - :summary: Show name and job tooltips near units on map. - :tags: fort inspection - -**IMPORTANT NOTE**: the tooltips will show over any vanilla UI elements! - - -This script shows "tooltips" in two optional modes: - -* following the mouse, when a unit is underneath the cursor; -* following units on the map. - -Information shown includes happiness indicator, name, and current job. - -Usage ------ - -:: - - gui/tooltips From bb4972ff0c75e66e057c7471c8bfddf480977dba Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Fri, 21 Feb 2025 21:08:15 +0100 Subject: [PATCH 435/811] Delete gui/tooltips.lua --- gui/tooltips.lua | 482 ----------------------------------------------- 1 file changed, 482 deletions(-) delete mode 100644 gui/tooltips.lua diff --git a/gui/tooltips.lua b/gui/tooltips.lua deleted file mode 100644 index abd9acf8d6..0000000000 --- a/gui/tooltips.lua +++ /dev/null @@ -1,482 +0,0 @@ --- Show tooltips on units and/or mouse - ---@ module = true - -local RELOAD = false -- set to true when actively working on this script - -local gui = require('gui') -local utils = require('utils') -local widgets = require('gui.widgets') -local overlay = require('plugins.overlay') - --------------------------------------------------------------------------------- - --- pens are the same as gui/control-panel.lua -local textures = require('gui.textures') -local function get_icon_pens() - local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} - local enabled_pen_center = dfhack.pen.parse{fg=COLOR_LIGHTGREEN, - tile=curry(textures.tp_control_panel, 2) or nil, ch=251} -- check - local enabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 3) or nil, ch=string.byte(']')} - local disabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 4) or nil, ch=string.byte('[')} - local disabled_pen_center = dfhack.pen.parse{fg=COLOR_RED, - tile=curry(textures.tp_control_panel, 5) or nil, ch=string.byte('x')} - local disabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 6) or nil, ch=string.byte(']')} - local button_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} - local button_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} - local help_pen_center = dfhack.pen.parse{ - tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} - local configure_pen_center = dfhack.pen.parse{ - tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol - return enabled_pen_left, enabled_pen_center, enabled_pen_right, - disabled_pen_left, disabled_pen_center, disabled_pen_right, - button_pen_left, button_pen_right, - help_pen_center, configure_pen_center -end -local ENABLED_PEN_LEFT, ENABLED_PEN_CENTER, ENABLED_PEN_RIGHT, - DISABLED_PEN_LEFT, DISABLED_PEN_CENTER, DISABLED_PEN_RIGHT, - BUTTON_PEN_LEFT, BUTTON_PEN_RIGHT, - HELP_PEN_CENTER, CONFIGURE_PEN_CENTER = get_icon_pens() - -if RELOAD then ToggleLabel = nil end -ToggleLabel = defclass(ToggleLabel, widgets.CycleHotkeyLabel) -ToggleLabel.ATTRS{ - options={{value=true}, - {value=false}}, -} -function ToggleLabel:init() - ToggleLabel.super.init(self) - - local text = self.text - -- the very last token is the On/Off text -- we'll repurpose it as an indicator - text[#text] = { tile = function() return self:getOptionValue() and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT end } - text[#text + 1] = { tile = function() return self:getOptionValue() and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER end } - text[#text + 1] = { tile = function() return self:getOptionValue() and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT end } - self:setText(text) -end - ---- - -if RELOAD then config = nil end -config = config or { - follow_units = true, - follow_mouse = false, - show_unit_jobs = true, - job_shortenings = { - ["Store item in stockpile"] = "Store item", - }, - show_happiness = true, - happiness_levels = { - -- keep in mind, the text will look differently with game's font - -- colors are same as in ASCII mode, but for then middle (3), which is GREY instead of WHITE - [0] = - {text = "=C", pen = COLOR_RED, visible = true, name = "Miserable"}, - {text = ":C", pen = COLOR_LIGHTRED, visible = true, name = "Unhappy"}, - {text = ":(", pen = COLOR_YELLOW, visible = false, name = "Displeased"}, - {text = ":]", pen = COLOR_GREY, visible = false, name = "Content"}, - {text = ":)", pen = COLOR_GREEN, visible = false, name = "Pleased"}, - {text = ":D", pen = COLOR_LIGHTGREEN, visible = true, name = "Happy"}, - {text = "=D", pen = COLOR_LIGHTCYAN, visible = true, name = "Ecstatic"}, - }, -} - --------------------------------------------------------------------------------- --- config persistence -local CONFIG_FILE_PATH = 'dfhack-config/tooltips.json' - -local function load_config() - local json = require('json') - - local f = json.open(CONFIG_FILE_PATH) - if f.exists then - -- remove unknown or out of date entries from the loaded config - -- shallow search should be enough - for k in pairs(f.data) do - if config[k] == nil then - f.data[k] = nil - end - end - - -- convert string keys into numbers - workaround json (encoder) limitations - ensure_key(f.data, "happiness_levels") - local t = f.data.happiness_levels - for k, v in pairs(t) do - t[tonumber(k)] = v - t[k] = nil - end - - utils.assign(config, f.data) - end - - f.data = config -- link the config info with the file - f:write() -- possibly update the stored config - return f -end - -local config_file = load_config() - --------------------------------------------------------------------------------- - -local TITLE = "Tooltips" - -if RELOAD then TooltipControlScreen = nil end -TooltipControlScreen = defclass(TooltipControlScreen, gui.ZScreen) -TooltipControlScreen.ATTRS { - focus_path = "TooltipControlScreen", - pass_movement_keys = true, -} - -function TooltipControlScreen:init() - local controls = TooltipControlWindow{view_id = 'controls'} - self:addviews{controls} -end - -function TooltipControlScreen:onDismiss() - view = nil -end - -if RELOAD then TooltipControlWindow = nil end -TooltipControlWindow = defclass(TooltipControlWindow, widgets.Window) -TooltipControlWindow.ATTRS { - frame_title=TITLE, - frame_inset=0, - resizable=false, - frame = { - w = 27, - h = 2 -- border - + 4 -- main options - + 7 -- happiness - , - -- just under the minimap: - r = 2, - t = 18, - }, -} - -local function make_enabled_text(indent, text, cfg, key) - local function get_enabled_button_token(enabled_tile, disabled_tile, cfg, key) - return { - tile=function() return cfg[key] and enabled_tile or disabled_tile end, - } - end - - local tokens = { - string.format("%" .. indent .. "s", ''), - get_enabled_button_token(ENABLED_PEN_LEFT, DISABLED_PEN_LEFT, cfg, key), - get_enabled_button_token(ENABLED_PEN_CENTER, DISABLED_PEN_CENTER, cfg, key), - get_enabled_button_token(ENABLED_PEN_RIGHT, DISABLED_PEN_RIGHT, cfg, key), - ' ', - } - if type(text) == 'string' then - tokens[#tokens+1] = text - else -- must be a table - -- append it - for _, v in ipairs(text) do - tokens[#tokens+1] = v - end - end - - return tokens -end - -local function make_choice(indent, text, cfg, key) - return { - text=make_enabled_text(indent, text, cfg, key), - data={cfg=cfg, key=key}, - } -end - -function TooltipControlWindow:init() - local choices = {} - table.insert(choices, make_choice(0, "unit banners", config, "follow_units")) - table.insert(choices, make_choice(0, "mouse tooltips", config, "follow_mouse")) - table.insert(choices, make_choice(0, "include jobs", config, "show_unit_jobs")) - table.insert(choices, make_choice(0, "include stress levels", config, "show_happiness")) - for i = 0, #config.happiness_levels do - local cfg = config.happiness_levels[i] - table.insert(choices, make_choice(3, {{text=cfg.text, pen=cfg.pen}, ' ', cfg.name}, cfg, "visible")) - end - - self:addviews{ - widgets.List{ - frame={t=0}, - view_id='list', - on_submit=self:callback('on_submit'), - row_height=1, - choices = choices, - }, - } -end - -function TooltipControlWindow:on_submit(index, choice) - local cfg = choice.data.cfg - local key = choice.data.key - cfg[key] = not cfg[key] - - config_file:write() -end - -local function GetUnitHappiness(unit) - if not config.show_happiness then return end - local stressCat = dfhack.units.getStressCategory(unit) - if stressCat > 6 then stressCat = 6 end - local happiness_level_cfg = config.happiness_levels[stressCat] - if not happiness_level_cfg.visible then return end - return happiness_level_cfg.text, happiness_level_cfg.pen -end - -local function GetUnitJob(unit) - if not config.show_unit_jobs then return end - local job = unit.job.current_job - return job and dfhack.job.getName(job) -end - -local function GetUnitNameAndJob(unit) - local sb = {} - sb[#sb+1] = dfhack.units.getReadableName(unit) - local jobName = GetUnitJob(unit) - if jobName then - sb[#sb+1] = ": " - sb[#sb+1] = jobName - end - return table.concat(sb) -end - -local function GetTooltipText(pos) - if not pos then return end - - local txt = {} - local units = dfhack.units.getUnitsInBox(pos, pos) or {} -- todo: maybe (optionally) use filter parameter here? - - for _,unit in ipairs(units) do - txt[#txt+1] = GetUnitNameAndJob(unit) - txt[#txt+1] = NEWLINE - end - - return txt -end - --------------------------------------------------------------------------------- --- MouseTooltip is an almost copy&paste of the DimensionsTooltip --- -if RELOAD then MouseTooltip = nil end -MouseTooltip = defclass(MouseTooltip, widgets.ResizingPanel) - -MouseTooltip.ATTRS{ - frame_style=gui.FRAME_THIN, - frame_background=gui.CLEAR_PEN, - no_force_pause_badge=true, - auto_width=true, - display_offset={x=3, y=3}, -} - -function MouseTooltip:init() - ensure_key(self, 'frame').w = 17 - self.frame.h = 4 - - self.label = widgets.Label{ - frame={t=0}, - auto_width=true, - } - - self:addviews{ - widgets.Panel{ - -- set minimum size for tooltip frame so the DFHack frame badge fits - frame={t=0, l=0, w=7, h=2}, - }, - self.label, - } -end - -function MouseTooltip:render(dc) - if not config.follow_mouse then return end - - local x, y = dfhack.screen.getMousePos() - if not x then return end - - local pos = dfhack.gui.getMousePos() - local text = GetTooltipText(pos) - if not text or #text == 0 then return end - self.label:setText(text) - - local sw, sh = dfhack.screen.getWindowSize() - local frame_width = math.max(9, self.label:getTextWidth() + 2) - self.frame.l = math.min(x + self.display_offset.x, sw - frame_width) - self.frame.t = math.min(y + self.display_offset.y, sh - self.frame.h) - self:updateLayout() - MouseTooltip.super.render(self, dc) -end - --------------------------------------------------------------------------------- -if RELOAD then TooltipsOverlay = nil end -TooltipsOverlay = defclass(TooltipsOverlay, overlay.OverlayWidget) -TooltipsOverlay.ATTRS{ - desc='Adds tooltips with some info to units.', - default_pos={x=1,y=1}, - default_enabled=true, - fullscreen=true, -- not player-repositionable - viewscreens={ - 'dwarfmode/Default', - }, -} - -function TooltipsOverlay:init() - local tooltip = MouseTooltip{view_id = 'tooltip'} - self:addviews{tooltip} -end - --- map coordinates -> interface layer coordinates -local function GetScreenCoordinates(map_coord) - -- -> map viewport offset - local vp = df.global.world.viewport - local vp_Coord = vp.corner - local map_offset_by_vp = { - x = map_coord.x - vp_Coord.x, - y = map_coord.y - vp_Coord.y, - z = map_coord.z - vp_Coord.z, - } - - if not dfhack.screen.inGraphicsMode() then - return map_offset_by_vp - else - -- -> pixel offset - local gps = df.global.gps - local map_tile_pixels = gps.viewport_zoom_factor // 4; - local screen_coord_px = { - x = map_tile_pixels * map_offset_by_vp.x, - y = map_tile_pixels * map_offset_by_vp.y, - } - -- -> interface layer coordinates - local screen_coord_text = { - x = math.ceil( screen_coord_px.x / gps.tile_pixel_x ), - y = math.ceil( screen_coord_px.y / gps.tile_pixel_y ), - } - - return screen_coord_text - end -end - -function TooltipsOverlay:render(dc) - self:render_unit_banners(dc) - TooltipsOverlay.super.render(self, dc) -end - -function TooltipsOverlay:render_unit_banners(dc) - if not config.follow_units then return end - - if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then - return - end - - local vp = df.global.world.viewport - local topleft = vp.corner - local width = vp.max_x - local height = vp.max_y - local bottomright = {x = topleft.x + width, y = topleft.y + height, z = topleft.z} - - local units = dfhack.units.getUnitsInBox(topleft, bottomright) - if not units or #units == 0 then return end - - local oneTileOffset = GetScreenCoordinates({x = topleft.x + 1, y = topleft.y + 1, z = topleft.z + 0}) - local pen = COLOR_WHITE - - local shortenings = config.job_shortenings - local used_tiles = {} - for i = #units, 1, -1 do - local unit = units[i] - - local happiness, happyPen = GetUnitHappiness(unit) - local job = GetUnitJob(unit) - job = shortenings[job] or job - if not job and not happiness then goto continue end - - local pos = xyz2pos(dfhack.units.getPosition(unit)) - if not pos then goto continue end - - local txt = (happiness and job and happiness .. " " .. job) - or happiness - or job - - local scrPos = GetScreenCoordinates(pos) - local y = scrPos.y - 1 -- subtract 1 to move the text over the heads - local x = scrPos.x + oneTileOffset.x - 1 -- subtract 1 to move the text inside the map tile - - -- to resolve overlaps, we'll mark every coordinate we write anything in, - -- and then check if the new tooltip will overwrite any used coordinate. - -- if it will, try the next row, to a maximum offset of 4. - local row - local dy = 0 - -- todo: search for the "best" offset instead, f.e. max `usedAt` value, with `-1` the best - local usedAt = -1 - for yOffset = 0, 4 do - dy = yOffset - - row = used_tiles[y + dy] - if not row then - row = {} - used_tiles[y + dy] = row - end - - usedAt = -1 - for j = 0, #txt - 1 do - if row[x + j] then - usedAt = j - break - end - end - - if usedAt == -1 then break end - end -- for dy - - -- in case there isn't enough space, cut the text off - if usedAt > 0 then - local s = happiness and #happiness + 1 or 0 - job = job:sub(0, usedAt - s - 1) .. '_' - txt = txt:sub(0, usedAt - 1) .. '_' -- for marking - end - - dc:seek(x, y + dy) - :pen(happyPen):string(happiness or "") - :string((happiness and job) and " " or "") - :pen(pen):string(job or "") - - -- mark coordinates as used - for j = 0, #txt - 1 do - row[x + j] = true - end - - ::continue:: - end -end - -function TooltipsOverlay:preUpdateLayout(parent_rect) - self.frame.w = parent_rect.width - self.frame.h = parent_rect.height -end - ----------------------------------------------------------------- - -OVERLAY_WIDGETS = { - tooltips=TooltipsOverlay, -} - -if dfhack_flags.module then - return -end - -if not dfhack.isMapLoaded() then - qerror('gui/tooltips requires a map to be loaded') -end - -if RELOAD and view then - view:dismiss() - -- view is nil now -end - -view = view and view:raise() or TooltipControlScreen{}:show() From 561eeab61f0ecfba1bcf094db361d99a2154dda9 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Fri, 21 Feb 2025 21:09:43 +0100 Subject: [PATCH 436/811] implement gui/spectate.lua --- gui/spectate.lua | 308 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 305 insertions(+), 3 deletions(-) diff --git a/gui/spectate.lua b/gui/spectate.lua index 4cea75855d..04191acd55 100644 --- a/gui/spectate.lua +++ b/gui/spectate.lua @@ -2,17 +2,319 @@ local gui = require('gui') local spectate = require('plugins.spectate') local widgets = require('gui.widgets') +-------------------------------------------------------------------------------- +--- ToggleLabel + +-- pens are the same as gui/control-panel.lua +local textures = require('gui.textures') +local function get_icon_pens() + local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} + local enabled_pen_center = dfhack.pen.parse{fg=COLOR_LIGHTGREEN, + tile=curry(textures.tp_control_panel, 2) or nil, ch=251} -- check + local enabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 3) or nil, ch=string.byte(']')} + local disabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 4) or nil, ch=string.byte('[')} + local disabled_pen_center = dfhack.pen.parse{fg=COLOR_RED, + tile=curry(textures.tp_control_panel, 5) or nil, ch=string.byte('x')} + local disabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 6) or nil, ch=string.byte(']')} + local button_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} + local button_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, + tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} + local help_pen_center = dfhack.pen.parse{ + tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} + local configure_pen_center = dfhack.pen.parse{ + tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol + return enabled_pen_left, enabled_pen_center, enabled_pen_right, + disabled_pen_left, disabled_pen_center, disabled_pen_right, + button_pen_left, button_pen_right, + help_pen_center, configure_pen_center +end +local ENABLED_PEN_LEFT, ENABLED_PEN_CENTER, ENABLED_PEN_RIGHT, + DISABLED_PEN_LEFT, DISABLED_PEN_CENTER, DISABLED_PEN_RIGHT, + BUTTON_PEN_LEFT, BUTTON_PEN_RIGHT, + HELP_PEN_CENTER, CONFIGURE_PEN_CENTER = get_icon_pens() + +ToggleLabel = defclass(ToggleLabel, widgets.CycleHotkeyLabel) +ToggleLabel.ATTRS{ + options={{value=true}, + {value=false}}, +} +function ToggleLabel:init() + ToggleLabel.super.init(self) + + local text = self.text + -- the very last token is the On/Off text -- we'll repurpose it as an indicator + text[#text] = { tile = function() return self:getOptionValue() and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT end } + text[#text + 1] = { tile = function() return self:getOptionValue() and ENABLED_PEN_CENTER or DISABLED_PEN_CENTER end } + text[#text + 1] = { tile = function() return self:getOptionValue() and ENABLED_PEN_RIGHT or DISABLED_PEN_RIGHT end } + self:setText(text) +end + +-------------------------------------------------------------------------------- +--- Spectate config window Spectate = defclass(Spectate, widgets.Window) Spectate.ATTRS { frame_title='Spectate', - frame={w=50, h=45}, + frame={w=35, h=30}, resizable=true, - resize_min={w=50, h=20}, + resize_min={w=35, h=30}, } +local function append(t, args) + for i = 1, #args do + t[#t + 1] = args[i] + end + return t +end + +local function create_toggle_button(cfg, cfg_key, left, top, hotkey, label) + return ToggleLabel{ + frame={t=top,l=left}, + initial_option = cfg[cfg_key], + on_change = function(new, old) cfg[cfg_key] = new; spectate.save_state() end, + key = hotkey, + label = label, + } +end + +local function create_numeric_edit_field(cfg, cfg_key, left, top, hotkey, label) + local editOnSubmit + local ef = widgets.EditField{ + frame={t=top,l=left}, + label_text = label, + text = tostring(cfg[cfg_key]), + modal = true, + key = hotkey, + on_char = function(new_char,text) return '0' <= new_char and new_char <= '9' end, + on_submit = function(text) editOnSubmit(text) end, + } + editOnSubmit = function(text) + if text == '' then + ef:setText(tostring(cfg[cfg_key])) + else + cfg[cfg_key] = tonumber(text) + spectate.save_state() + end + end + + return ef +end + +local function create_toggle_buttons(cfgFollow, keyFollow, cfgHover, keyHover, colFollow, colHover, top) + local tlFollow = create_toggle_button(cfgFollow, keyFollow, colFollow + 2, top) + local tlHover = create_toggle_button(cfgHover, keyHover, colHover + 1, top) + + return tlFollow, tlHover +end + +local function create_row(label, hotkey, suffix, colFollow, colHover, top) + local config = spectate.config + + suffix = suffix or '' + if suffix ~= '' then suffix = '-'..suffix end + + local keyFollow = 'tooltip-follow'..suffix + local keyHover = 'tooltip-hover'..suffix + + local tlFollow, tlHover = create_toggle_buttons(config, keyFollow, config, keyHover, colFollow, colHover, top) + local views = { + widgets.HotkeyLabel{ + frame={t=top,l=0,w=1}, + key = 'CUSTOM_' .. hotkey, + key_sep = '', + on_activate = function() tlFollow:cycle() end, + }, + widgets.HotkeyLabel{ + frame={t=top,l=1,w=1}, + key = 'CUSTOM_SHIFT_' .. hotkey, + key_sep = '', + on_activate = function() tlHover:cycle() end, + }, + widgets.Label{ + frame={t=top,l=2}, + text = ': ' .. label, + }, + tlFollow, + tlHover, + } + + return views +end + +local function make_choice(text, tlFollow, tlHover) + return { + text=text, + data={tlFollow=tlFollow, tlHover=tlHover}, + } +end + +local function pairsByKeys(t, f) + local a = {} + for n in pairs(t) do table.insert(a, n) end + table.sort(a, f) + local i = 0 -- iterator variable + local iter = function () -- iterator function + i = i + 1 + if a[i] == nil then return nil + else return a[i], t[a[i]] + end + end + return iter +end + +local function rpad(s, i) + return string.format("%-"..i.."s", s) +end + +local overlay = require('plugins.overlay') +local OVERLAY_NAME = 'spectate.tooltip' +local function isOverlayEnabled() + return overlay.get_state().config[OVERLAY_NAME].enabled +end + +local function enable_overlay(enabled) + local tokens = {'overlay'} + table.insert(tokens, enabled and 'enable' or 'disable') + table.insert(tokens, OVERLAY_NAME) + dfhack.run_command(tokens) +end + +function Spectate:updateOverlayDisabledGag(widget) + local w = widget or self.subviews.overlayIsDisabledGag + + if isOverlayEnabled() then + if w.frame.t < 500 then + w.frame.t = w.frame.t + 500 + end + else + if w.frame.t > 500 then + w.frame.t = w.frame.t - 500 + end + end + + if not widget then + self:updateLayout() + end +end + function Spectate:init() - self:addviews{ + local config = spectate.config + + local views = {} + local t = 0 + + local len = 20 + append(views, {create_toggle_button(config, 'auto-disengage', 0, t, 'CUSTOM_ALT_D', rpad("Auto disengage", len))}); t = t + 1 + append(views, {create_toggle_button(config, 'auto-unpause', 0, t, 'CUSTOM_ALT_U', rpad("Auto unpause", len))}); t = t + 1 + append(views, {create_toggle_button(config, 'cinematic-action', 0, t, 'CUSTOM_ALT_C', rpad("Cinematic action", len))}); t = t + 1 + append(views, {create_numeric_edit_field(config, 'follow-seconds', 0, t, 'CUSTOM_ALT_F', "Follow (s): ")}); t = t + 1 + append(views, {create_toggle_button(config, 'include-animals', 0, t, 'CUSTOM_ALT_A', rpad("Include animals", len))}); t = t + 1 + append(views, {create_toggle_button(config, 'include-hostiles', 0, t, 'CUSTOM_ALT_H', rpad("Include hostiles", len))}); t = t + 1 + append(views, {create_toggle_button(config, 'include-visitors', 0, t, 'CUSTOM_ALT_V', rpad("Include visitors", len))}); t = t + 1 + append(views, {create_toggle_button(config, 'include-wildlife', 0, t, 'CUSTOM_ALT_W', rpad("Include wildlife", len))}); t = t + 1 + append(views, {create_toggle_button(config, 'prefer-conflict', 0, t, 'CUSTOM_ALT_B', rpad("Prefer conflict", len))}); t = t + 1 + append(views, {create_toggle_button(config, 'prefer-new-arrivals', 0, t, 'CUSTOM_ALT_N', rpad("Prefer new arrivals", len))}); t = t + 1 + + t = t + 1 -- add a blank line + local colFollow, colHover = 15, 25 + -- tooltips headers + append(views, { + widgets.Label{ + frame={t=t,l=0}, + text="Tooltips:" + }, + }) + -- t = t + 1 + -- overlay is prerequisite for any other tooltip option + local overlayIsDisabledGag = nil + local lblOverlayOnChange = function(new, old) + enable_overlay(new) + self:updateOverlayDisabledGag() + end + append(views, { + ToggleLabel{ + frame={t=t,l=12}, + initial_option = isOverlayEnabled(), + on_change = lblOverlayOnChange, + key = 'CUSTOM_ALT_O', + label = "Overlay ", + } + }) + t = t + 1 + overlayIsDisabledGag = widgets.Panel{ + view_id='overlayIsDisabledGag', + frame={t=t,l=0,r=0,b=1}, -- b=1 because the very last row is where the HelpButton is placed + frame_background=gui.CLEAR_PEN, + subviews = { + widgets.WrappedLabel{ + frame={t=0,l=0,r=0,b=0}, + frame_background=gui.CLEAR_PEN, + text_to_wrap="Overlay has to be enabled for tooltips.", + } + }, } + + append(views, { + widgets.Label{ + frame={t=t,l=colFollow}, + text="Follow" + }, + widgets.Label{ + frame={t=t,l=colHover}, + text="Hover" + }, + }) + t = t + 1 + + -- enable/disable + append(views, create_row("Enable", 'E', '', colFollow, colHover, t)); t = t + 1 + append(views, create_row("Job", 'J', 'job', colFollow, colHover, t)); t = t + 1 + append(views, create_row("Name", 'N', 'name', colFollow, colHover, t)); t = t + 1 + append(views, create_row("Stress", 'S', 'stress', colFollow, colHover, t)); t = t + 1 + + -- next are individual stress levels + -- a list on the left to select one, individual buttons in two columns to be able to click on them + local choices = {} + local levels = config['tooltip-stress-levels'] + local stressFollow = config['tooltip-follow-stress-levels'] + local stressHover = config['tooltip-hover-stress-levels'] + local tList = t + for l, cfg in pairsByKeys(levels) do + local tlFollow, tlHover = create_toggle_buttons(stressFollow, l, stressHover, l, colFollow, colHover, t) + append(views, { tlFollow, tlHover }) + + table.insert(choices, make_choice({{text=cfg.text, pen=cfg.pen}, ' ', cfg.name}, tlFollow, tlHover)) + + t = t + 1 + end + append(views,{ + widgets.List{ + frame={t=tList,l=2}, + view_id='list_levels', + on_submit=function(index, choice) choice.data.tlFollow:cycle() end, + on_submit2=function(index, choice) choice.data.tlHover:cycle() end, + row_height=1, + choices = choices, + }, + }) + + append(views, {create_numeric_edit_field(config, 'tooltip-follow-blink-milliseconds', 0, t, 'CUSTOM_B', "Blink duration (ms): ")}); t = t + 1 + + append(views, { + widgets.HelpButton{ + frame={b=0,r=0}, + command = 'spectate', + } + }) + + append(views, {overlayIsDisabledGag}) -- must be the very last thing + self:updateOverlayDisabledGag(overlayIsDisabledGag) + + self:addviews(views) end SpectateScreen = defclass(SpectateScreen, gui.ZScreen) From 3c3fa0575d3b9c0876431b349643ea232c95bd04 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Fri, 21 Feb 2025 21:24:42 +0100 Subject: [PATCH 437/811] adjust starting position --- gui/spectate.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/spectate.lua b/gui/spectate.lua index 04191acd55..dabfe5b42e 100644 --- a/gui/spectate.lua +++ b/gui/spectate.lua @@ -59,7 +59,7 @@ end Spectate = defclass(Spectate, widgets.Window) Spectate.ATTRS { frame_title='Spectate', - frame={w=35, h=30}, + frame={l=3, t=5, w=35, h=30}, resizable=true, resize_min={w=35, h=30}, } From 0d8e540a2bd432267ab31994589c89843f4c03af Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 21 Feb 2025 23:01:47 -0800 Subject: [PATCH 438/811] don't include already prioritized jobs in output of -j and print number of already-prioritized jobs when prioritizing all jobs of a specified type --- changelog.txt | 2 ++ docs/prioritize.rst | 10 +++++----- prioritize.lua | 15 ++++++++++++--- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index 97d2ed0c7a..57fad9d45d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -45,6 +45,8 @@ Template for new versions: - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete - `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. - `gui/sitemap`: shift click to start following the selected unit or artifact +- `prioritize`: when prioritizing jobs of a specified type, also output how many of those jobs were already prioritized before you ran the command +- `prioritize`: don't include already-prioritized jobs in the output of ``prioritize -j`` ## Removed diff --git a/docs/prioritize.rst b/docs/prioritize.rst index acc1d55f65..bdfb5aca2a 100644 --- a/docs/prioritize.rst +++ b/docs/prioritize.rst @@ -7,7 +7,7 @@ prioritize This tool encourages specified types of jobs to get assigned and completed as soon as possible. Finally, you can be sure your food will be hauled before -rotting, your hides will be tanned before going bad, and the corpses of your +it rots, your hides will be tanned before they go bad, and the corpses of your enemies will be cleared expediently from your entranceway. You can prioritize a bunch of active jobs that you need done *right now*, or you @@ -40,7 +40,8 @@ Examples Watch for and prioritize the default set of job types that the community has suggested and playtested (see below for details). ``prioritize -j`` - Print out the list of active jobs that you can prioritize right now. + Print out the list of not-yet prioritized jobs that you can prioritize + right now. ``prioritize ConstructBuilding DestroyBuilding`` Prioritize all current building construction and destruction jobs. ``prioritize -a --haul-labor=Food,Body StoreItemInStockpile`` @@ -56,8 +57,7 @@ Options ``-d``, ``--delete`` Stop automatically prioritizing new jobs of the specified job types. ``-j``, ``--jobs`` - Print out how many current jobs of each type there are. This is useful for - discovering the types of the jobs that you can prioritize right now. If any + Print out how many current unprioritized jobs of each type there are. If any job types are specified, only jobs of those types are listed. ``-l``, ``--haul-labor [,...]`` For StoreItemInStockpile jobs, match only the specified hauling labor(s). @@ -97,7 +97,7 @@ staring at the screen in annoyance for too long. You may be tempted to automatically prioritize ``ConstructBuilding`` jobs, but beware that if you engage in megaprojects where many constructions must be built, these jobs can consume your entire fortress if prioritized. It is often -better to run ``prioritize ConstructBuilding`` by itself (i.e. without the +better to run ``prioritize ConstructBuilding`` by itself (that is, without the ``-a`` parameter) as needed to just prioritize the construction jobs that you have ready at the time if you need to "clear the queue". diff --git a/prioritize.lua b/prioritize.lua index 269a8a3acc..bd52d5ff09 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -192,15 +192,23 @@ local function for_all_jobs(cb) end local function boost(job_matchers, opts) - local count = 0 + local count, already_prioritized = 0, 0 for_all_jobs( function(job) - if not job.flags.do_now and boost_job_if_matches(job, job_matchers) then - count = count + 1 + local was_prioritized = job.flags.do_now + if boost_job_if_matches(job, job_matchers) then + if was_prioritized then + already_prioritized = already_prioritized + 1 + else + count = count + 1 + end end end) if not opts.quiet then print(('Prioritized %d job%s.'):format(count, count == 1 and '' or 's')) + if already_prioritized > 0 then + print(('%d job%s already prioritized.'):format(already_prioritized, already_prioritized == 1 and '' or 's')) + end end end @@ -440,6 +448,7 @@ local function print_current_jobs(job_matchers, opts) local filtered = next(job_matchers) local function count_job(jobs, job) if filtered and not job_matchers[job.job_type] then return end + if job.flags.do_now then return end local job_type = get_job_type_str(job) jobs[job_type] = (jobs[job_type] or 0) + 1 end From 637eb47ca98e9188fe7cdd73c73b0d994c4bada8 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 22 Feb 2025 01:43:39 -0600 Subject: [PATCH 439/811] Add save reminder to Adv mode and change colors based on how long it has been since a save (#1384) use `:lua reqscript('internal/notify/notifications').save_time_threshold_mins=X` to set the threshold to X mins --------- Co-authored-by: Myk --- changelog.txt | 2 ++ internal/notify/notifications.lua | 47 ++++++++++++++++++++++--------- 2 files changed, 36 insertions(+), 13 deletions(-) diff --git a/changelog.txt b/changelog.txt index 57fad9d45d..0f7957bc0f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -42,6 +42,8 @@ Template for new versions: - `hide-tutorials`: handle tutorial popups for adventure mode - `hide-tutorials`: new ``reset`` command that will re-enable popups in the current game (in case you hid them all and now want them back) - `gui/notify`: moody dwarf notification turns red when they can't reach workshop or items +- `gui/notify`: save reminder now appears in adventure mode +- `gui/notify`: save reminder changes color to yellow at 30 minutes and to orange at 60 minutes - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete - `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. - `gui/sitemap`: shift click to start following the selected unit or artifact diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index d51d930100..8af7c2c187 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -14,6 +14,10 @@ local buildings = df.global.world.buildings local caravans = df.global.plotinfo.caravans local units = df.global.world.units +-- TODO: Add a proper API and UI for notification configuration +-- this is global so one can use `:lua reqscript('internal/notify/notifications').save_time_threshold_mins=X` to change the threshold to X mins. +save_time_threshold_mins = save_time_threshold_mins or 15 + function for_iter(vec, match_fn, action_fn, reverse) local offset = type(vec) == 'table' and 1 or 0 local idx1 = reverse and #vec-1+offset or offset @@ -303,6 +307,33 @@ local function get_bar(get_fn, get_max_fn, text, color) return nil end +local function get_save_alert() + local mins_since_save = dfhack.persistent.getUnsavedSeconds()//60 + local pen = COLOR_LIGHTCYAN + if mins_since_save < save_time_threshold_mins then return end + if mins_since_save >= 4*save_time_threshold_mins then + pen = COLOR_LIGHTRED + elseif mins_since_save >= 2*save_time_threshold_mins then + pen = COLOR_YELLOW + end + return { + {text='Last save: ', pen=COLOR_WHITE}, + {text=dfhack.formatInt(mins_since_save) ..' mins ago', pen=pen}, + } +end + +local function save_popup() + local mins_since_save = dfhack.persistent.getUnsavedSeconds()//60 + local message = 'It has been ' .. dfhack.formatInt(mins_since_save) .. ' minutes since your last save.' + if dfhack.world.isFortressMode() then + message = message .. '\n\nWould you like to save now? (Note: You can also close this reminder and save manually)' + dlg.showYesNoPrompt('Save now?', message, nil, function() dfhack.run_script('quicksave') end) + else + message = message .. '\n\nClose this popup to open the options menu and select "Save and continue playing"' + dlg.showMessage('Save reminder', message, COLOR_WHITE, function() gui.simulateInput(dfhack.gui.getDFViewscreen(true), 'OPTIONS') end) + end +end + -- the order of this list controls the order the notifications will appear in the overlay NOTIFICATIONS_BY_IDX = { { @@ -526,20 +557,10 @@ NOTIFICATIONS_BY_IDX = { }, { name='save-reminder', - desc='Shows a reminder if it has been more than 15 minutes since your last save.', + desc=('Shows a reminder if it has been more than %d minute%s since your last save.'):format(save_time_threshold_mins, save_time_threshold_mins == 1 and '' or 's'), default=true, - dwarf_fn=function () - local minsSinceSave = dfhack.persistent.getUnsavedSeconds()//60 - if minsSinceSave >= 15 then - return "Last save: ".. (dfhack.formatInt(minsSinceSave)) ..' mins ago' - end - end, - on_click=function() - local minsSinceSave = dfhack.persistent.getUnsavedSeconds()//60 - local message = 'It has been ' .. dfhack.formatInt(minsSinceSave) .. ' minutes since your last save. \n\nWould you like to save now?\n\n' .. - 'You can also close this reminder and save manually.' - dlg.showYesNoPrompt('Save now?', message, nil, function() dfhack.run_script('quicksave') end) - end, + fn=get_save_alert, + on_click=save_popup, }, } From 46689c23bb9b338a9e785c481221630c8694e265 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 02:44:46 -0800 Subject: [PATCH 440/811] refactor UI and use upstreamed functionality --- docs/gui/spectate.rst | 3 + gui/spectate.lua | 354 ++++++++++++++++-------------------------- 2 files changed, 140 insertions(+), 217 deletions(-) diff --git a/docs/gui/spectate.rst b/docs/gui/spectate.rst index e6034cbf80..802d4d3dac 100644 --- a/docs/gui/spectate.rst +++ b/docs/gui/spectate.rst @@ -8,6 +8,9 @@ gui/spectate This is an in-game configuration interface for `spectate`, which automatically sets the camera to follow interesting units. +You can configure the overlay tooltip settings as well as the follow mode +settings. + Usage ----- diff --git a/gui/spectate.lua b/gui/spectate.lua index dabfe5b42e..3c708be7a1 100644 --- a/gui/spectate.lua +++ b/gui/spectate.lua @@ -1,12 +1,16 @@ local gui = require('gui') +local overlay = require('plugins.overlay') local spectate = require('plugins.spectate') +local textures = require('gui.textures') +local utils = require('utils') local widgets = require('gui.widgets') +local OVERLAY_NAME = 'spectate.tooltip' + -------------------------------------------------------------------------------- --- ToggleLabel -- pens are the same as gui/control-panel.lua -local textures = require('gui.textures') local function get_icon_pens() local enabled_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, tile=curry(textures.tp_control_panel, 1), ch=string.byte('[')} @@ -20,32 +24,15 @@ local function get_icon_pens() tile=curry(textures.tp_control_panel, 5) or nil, ch=string.byte('x')} local disabled_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, tile=curry(textures.tp_control_panel, 6) or nil, ch=string.byte(']')} - local button_pen_left = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 7) or nil, ch=string.byte('[')} - local button_pen_right = dfhack.pen.parse{fg=COLOR_CYAN, - tile=curry(textures.tp_control_panel, 8) or nil, ch=string.byte(']')} - local help_pen_center = dfhack.pen.parse{ - tile=curry(textures.tp_control_panel, 9) or nil, ch=string.byte('?')} - local configure_pen_center = dfhack.pen.parse{ - tile=curry(textures.tp_control_panel, 10) or nil, ch=15} -- gear/masterwork symbol return enabled_pen_left, enabled_pen_center, enabled_pen_right, - disabled_pen_left, disabled_pen_center, disabled_pen_right, - button_pen_left, button_pen_right, - help_pen_center, configure_pen_center + disabled_pen_left, disabled_pen_center, disabled_pen_right end local ENABLED_PEN_LEFT, ENABLED_PEN_CENTER, ENABLED_PEN_RIGHT, - DISABLED_PEN_LEFT, DISABLED_PEN_CENTER, DISABLED_PEN_RIGHT, - BUTTON_PEN_LEFT, BUTTON_PEN_RIGHT, - HELP_PEN_CENTER, CONFIGURE_PEN_CENTER = get_icon_pens() + DISABLED_PEN_LEFT, DISABLED_PEN_CENTER, DISABLED_PEN_RIGHT = get_icon_pens() -ToggleLabel = defclass(ToggleLabel, widgets.CycleHotkeyLabel) -ToggleLabel.ATTRS{ - options={{value=true}, - {value=false}}, -} -function ToggleLabel:init() - ToggleLabel.super.init(self) +ToggleLabel = defclass(ToggleLabel, widgets.ToggleHotkeyLabel) +function ToggleLabel:init() local text = self.text -- the very last token is the On/Off text -- we'll repurpose it as an indicator text[#text] = { tile = function() return self:getOptionValue() and ENABLED_PEN_LEFT or DISABLED_PEN_LEFT end } @@ -59,90 +46,79 @@ end Spectate = defclass(Spectate, widgets.Window) Spectate.ATTRS { frame_title='Spectate', - frame={l=3, t=5, w=35, h=30}, - resizable=true, - resize_min={w=35, h=30}, + frame={l=5, t=5, w=36, h=39}, } -local function append(t, args) - for i = 1, #args do - t[#t + 1] = args[i] - end - return t -end - -local function create_toggle_button(cfg, cfg_key, left, top, hotkey, label) +local function create_toggle_button(frame, cfg_elem, hotkey, label, cfg_elem_key) return ToggleLabel{ - frame={t=top,l=left}, - initial_option = cfg[cfg_key], - on_change = function(new, old) cfg[cfg_key] = new; spectate.save_state() end, - key = hotkey, - label = label, + frame=frame, + initial_option=spectate.get_config_elem(cfg_elem, cfg_elem_key), + on_change=function(val) dfhack.run_command('spectate', 'set', cfg_elem, tostring(val)) end, + key=hotkey, + label=label, } end -local function create_numeric_edit_field(cfg, cfg_key, left, top, hotkey, label) +local function create_numeric_edit_field(frame, cfg_elem, hotkey, label) local editOnSubmit local ef = widgets.EditField{ - frame={t=top,l=left}, + frame=frame, label_text = label, - text = tostring(cfg[cfg_key]), + text = tostring(spectate.get_config_elem(cfg_elem)), modal = true, key = hotkey, - on_char = function(new_char,text) return '0' <= new_char and new_char <= '9' end, + on_char = function(ch) return ch:match('%d') end, on_submit = function(text) editOnSubmit(text) end, } editOnSubmit = function(text) if text == '' then - ef:setText(tostring(cfg[cfg_key])) + ef:setText(tostring(spectate.get_config_elem(cfg_elem))) else - cfg[cfg_key] = tonumber(text) - spectate.save_state() + dfhack.run_command('spectate', 'set', cfg_elem, text) end end return ef end -local function create_toggle_buttons(cfgFollow, keyFollow, cfgHover, keyHover, colFollow, colHover, top) - local tlFollow = create_toggle_button(cfgFollow, keyFollow, colFollow + 2, top) - local tlHover = create_toggle_button(cfgHover, keyHover, colHover + 1, top) - +local function create_row_toggle_buttons(keyFollow, keyHover, colFollow, colHover, cfg_elem_key) + local tlFollow = create_toggle_button({l=colFollow+2}, keyFollow, nil, nil, cfg_elem_key) + local tlHover = create_toggle_button({l=colHover+1}, keyHover, nil, nil, cfg_elem_key) return tlFollow, tlHover end -local function create_row(label, hotkey, suffix, colFollow, colHover, top) - local config = spectate.config - +local function create_row(frame, label, hotkey, suffix, colFollow, colHover) suffix = suffix or '' if suffix ~= '' then suffix = '-'..suffix end local keyFollow = 'tooltip-follow'..suffix local keyHover = 'tooltip-hover'..suffix - local tlFollow, tlHover = create_toggle_buttons(config, keyFollow, config, keyHover, colFollow, colHover, top) - local views = { - widgets.HotkeyLabel{ - frame={t=top,l=0,w=1}, - key = 'CUSTOM_' .. hotkey, - key_sep = '', - on_activate = function() tlFollow:cycle() end, - }, - widgets.HotkeyLabel{ - frame={t=top,l=1,w=1}, - key = 'CUSTOM_SHIFT_' .. hotkey, - key_sep = '', - on_activate = function() tlHover:cycle() end, + local tlFollow, tlHover = create_row_toggle_buttons(keyFollow, keyHover, colFollow, colHover) + + return widgets.Panel{ + frame=utils.assign({h=1}, frame), + subviews={ + widgets.HotkeyLabel{ + frame={l=0,w=1}, + key='CUSTOM_' .. hotkey, + key_sep='', + on_activate=function() tlFollow:cycle() end, + }, + widgets.HotkeyLabel{ + frame={l=1,w=1}, + key='CUSTOM_SHIFT_' .. hotkey, + key_sep='', + on_activate=function() tlHover:cycle() end, + }, + widgets.Label{ + frame={l=2}, + text = ': ' .. label, + }, + tlFollow, + tlHover, }, - widgets.Label{ - frame={t=top,l=2}, - text = ': ' .. label, - }, - tlFollow, - tlHover, } - - return views end local function make_choice(text, tlFollow, tlHover) @@ -152,169 +128,113 @@ local function make_choice(text, tlFollow, tlHover) } end -local function pairsByKeys(t, f) - local a = {} - for n in pairs(t) do table.insert(a, n) end - table.sort(a, f) - local i = 0 -- iterator variable - local iter = function () -- iterator function - i = i + 1 - if a[i] == nil then return nil - else return a[i], t[a[i]] - end - end - return iter -end +-- individual stress levels +-- a list on the left to select one, individual buttons in two columns to be able to click on them +local function create_stress_list(frame, colFollow, colHover) + local levelsKey = 'tooltip-stress-levels' + local stressFollowKey = 'tooltip-follow-stress-levels' + local stressHoverKey = 'tooltip-hover-stress-levels' + + local choices, subviews = {}, {} + for idx=0,6 do + local cfgElemKey = tostring(idx) + local tlFollow, tlHover = create_row_toggle_buttons(stressFollowKey, stressHoverKey, colFollow, colHover, cfgElemKey) + table.insert(subviews, widgets.Panel{ + frame={t=idx, h=1}, + subviews={ + tlFollow, + tlHover, + } + }) -local function rpad(s, i) - return string.format("%-"..i.."s", s) -end + local elem = spectate.get_config_elem(levelsKey, cfgElemKey) + table.insert(choices, make_choice({{text=elem.text, pen=elem.pen}, ' ', elem.name}, tlFollow, tlHover)) + end -local overlay = require('plugins.overlay') -local OVERLAY_NAME = 'spectate.tooltip' -local function isOverlayEnabled() - return overlay.get_state().config[OVERLAY_NAME].enabled -end + table.insert(subviews, widgets.List{ + frame={l=2}, + on_submit=function(_, choice) choice.data.tlFollow:cycle() end, + on_submit2=function(_, choice) choice.data.tlHover:cycle() end, + choices=choices, + }) -local function enable_overlay(enabled) - local tokens = {'overlay'} - table.insert(tokens, enabled and 'enable' or 'disable') - table.insert(tokens, OVERLAY_NAME) - dfhack.run_command(tokens) + return widgets.Panel{ + frame=frame, + subviews=subviews, + } end -function Spectate:updateOverlayDisabledGag(widget) - local w = widget or self.subviews.overlayIsDisabledGag - - if isOverlayEnabled() then - if w.frame.t < 500 then - w.frame.t = w.frame.t + 500 - end - else - if w.frame.t > 500 then - w.frame.t = w.frame.t - 500 - end - end - - if not widget then - self:updateLayout() - end +local function rpad(s, i) + return string.format("%-"..i.."s", s) end function Spectate:init() - local config = spectate.config - - local views = {} - local t = 0 - - local len = 20 - append(views, {create_toggle_button(config, 'auto-disengage', 0, t, 'CUSTOM_ALT_D', rpad("Auto disengage", len))}); t = t + 1 - append(views, {create_toggle_button(config, 'auto-unpause', 0, t, 'CUSTOM_ALT_U', rpad("Auto unpause", len))}); t = t + 1 - append(views, {create_toggle_button(config, 'cinematic-action', 0, t, 'CUSTOM_ALT_C', rpad("Cinematic action", len))}); t = t + 1 - append(views, {create_numeric_edit_field(config, 'follow-seconds', 0, t, 'CUSTOM_ALT_F', "Follow (s): ")}); t = t + 1 - append(views, {create_toggle_button(config, 'include-animals', 0, t, 'CUSTOM_ALT_A', rpad("Include animals", len))}); t = t + 1 - append(views, {create_toggle_button(config, 'include-hostiles', 0, t, 'CUSTOM_ALT_H', rpad("Include hostiles", len))}); t = t + 1 - append(views, {create_toggle_button(config, 'include-visitors', 0, t, 'CUSTOM_ALT_V', rpad("Include visitors", len))}); t = t + 1 - append(views, {create_toggle_button(config, 'include-wildlife', 0, t, 'CUSTOM_ALT_W', rpad("Include wildlife", len))}); t = t + 1 - append(views, {create_toggle_button(config, 'prefer-conflict', 0, t, 'CUSTOM_ALT_B', rpad("Prefer conflict", len))}); t = t + 1 - append(views, {create_toggle_button(config, 'prefer-new-arrivals', 0, t, 'CUSTOM_ALT_N', rpad("Prefer new arrivals", len))}); t = t + 1 - - t = t + 1 -- add a blank line + local lWidth = 21 local colFollow, colHover = 15, 25 - -- tooltips headers - append(views, { + + self:addviews{ widgets.Label{ - frame={t=t,l=0}, - text="Tooltips:" + frame={t=0, l=0}, + text='See help for option details:', + }, + widgets.HelpButton{ + frame={t=0, r=0}, + command = 'spectate', }, - }) - -- t = t + 1 - -- overlay is prerequisite for any other tooltip option - local overlayIsDisabledGag = nil - local lblOverlayOnChange = function(new, old) - enable_overlay(new) - self:updateOverlayDisabledGag() - end - append(views, { ToggleLabel{ - frame={t=t,l=12}, - initial_option = isOverlayEnabled(), - on_change = lblOverlayOnChange, - key = 'CUSTOM_ALT_O', - label = "Overlay ", - } - }) - t = t + 1 - overlayIsDisabledGag = widgets.Panel{ - view_id='overlayIsDisabledGag', - frame={t=t,l=0,r=0,b=1}, -- b=1 because the very last row is where the HelpButton is placed - frame_background=gui.CLEAR_PEN, - subviews = { - widgets.WrappedLabel{ - frame={t=0,l=0,r=0,b=0}, - frame_background=gui.CLEAR_PEN, - text_to_wrap="Overlay has to be enabled for tooltips.", - } + frame={t=2}, + view_id='spectate_mode', + initial_option=spectate.isEnabled(), + on_change=function(val) dfhack.run_command(val and 'enable' or 'disable', 'spectate') end, + key='CUSTOM_ALT_E', + label='Spectate mode ', + }, + create_numeric_edit_field({t=4}, 'follow-seconds', 'CUSTOM_ALT_F', 'Switch target (sec): '), + create_toggle_button({t=6}, 'auto-unpause', 'CUSTOM_ALT_U', rpad('Auto unpause', lWidth)), + create_toggle_button({t=7}, 'cinematic-action', 'CUSTOM_ALT_C', rpad('Cinematic action', lWidth)), + create_toggle_button({t=8}, 'include-animals', 'CUSTOM_ALT_A', rpad('Include animals', lWidth)), + create_toggle_button({t=9}, 'include-hostiles', 'CUSTOM_ALT_H', rpad('Include hostiles', lWidth)), + create_toggle_button({t=10}, 'include-visitors', 'CUSTOM_ALT_V', rpad('Include visitors', lWidth)), + create_toggle_button({t=11}, 'include-wildlife', 'CUSTOM_ALT_W', rpad('Include wildlife', lWidth)), + create_toggle_button({t=12}, 'prefer-conflict', 'CUSTOM_ALT_B', rpad('Prefer conflict', lWidth)), + create_toggle_button({t=13}, 'prefer-new-arrivals', 'CUSTOM_ALT_N', rpad('Prefer new arrivals', lWidth)), + widgets.Divider{ + frame={t=15, h=1}, + frame_style=gui.FRAME_THIN, + frame_style_l=false, + frame_style_r=false, }, - } - - append(views, { widgets.Label{ - frame={t=t,l=colFollow}, - text="Follow" + frame={t=17, l=0}, + text="Tooltips:" + }, + ToggleLabel{ + frame={t=17, l=12}, + initial_option=overlay.isOverlayEnabled(OVERLAY_NAME), + on_change=function(val) dfhack.run_command('overlay', val and 'enable' or 'disable', OVERLAY_NAME) end, + key='CUSTOM_ALT_O', + label="Overlay ", }, widgets.Label{ - frame={t=t,l=colHover}, - text="Hover" + frame={t=19, l=colFollow}, + text='Follow', }, - }) - t = t + 1 - - -- enable/disable - append(views, create_row("Enable", 'E', '', colFollow, colHover, t)); t = t + 1 - append(views, create_row("Job", 'J', 'job', colFollow, colHover, t)); t = t + 1 - append(views, create_row("Name", 'N', 'name', colFollow, colHover, t)); t = t + 1 - append(views, create_row("Stress", 'S', 'stress', colFollow, colHover, t)); t = t + 1 - - -- next are individual stress levels - -- a list on the left to select one, individual buttons in two columns to be able to click on them - local choices = {} - local levels = config['tooltip-stress-levels'] - local stressFollow = config['tooltip-follow-stress-levels'] - local stressHover = config['tooltip-hover-stress-levels'] - local tList = t - for l, cfg in pairsByKeys(levels) do - local tlFollow, tlHover = create_toggle_buttons(stressFollow, l, stressHover, l, colFollow, colHover, t) - append(views, { tlFollow, tlHover }) - - table.insert(choices, make_choice({{text=cfg.text, pen=cfg.pen}, ' ', cfg.name}, tlFollow, tlHover)) - - t = t + 1 - end - append(views,{ - widgets.List{ - frame={t=tList,l=2}, - view_id='list_levels', - on_submit=function(index, choice) choice.data.tlFollow:cycle() end, - on_submit2=function(index, choice) choice.data.tlHover:cycle() end, - row_height=1, - choices = choices, + widgets.Label{ + frame={t=19, l=colHover}, + text='Hover', }, - }) - - append(views, {create_numeric_edit_field(config, 'tooltip-follow-blink-milliseconds', 0, t, 'CUSTOM_B', "Blink duration (ms): ")}); t = t + 1 - - append(views, { - widgets.HelpButton{ - frame={b=0,r=0}, - command = 'spectate', - } - }) - - append(views, {overlayIsDisabledGag}) -- must be the very last thing - self:updateOverlayDisabledGag(overlayIsDisabledGag) + create_row({t=21}, 'Enabled', 'E', '', colFollow, colHover), + create_numeric_edit_field({t=23}, 'tooltip-follow-blink-milliseconds', 'CUSTOM_B', 'Blink period (ms): '), + create_row({t=25}, 'Job', 'J', 'job', colFollow, colHover), + create_row({t=26}, 'Name', 'N', 'name', colFollow, colHover), + create_row({t=27}, 'Stress', 'S', 'stress', colFollow, colHover), + create_stress_list({t=28}, colFollow, colHover), + } +end - self:addviews(views) +function Spectate:render(dc) + self.subviews.spectate_mode:setOption(spectate.isEnabled()) + Spectate.super.render(self, dc) end SpectateScreen = defclass(SpectateScreen, gui.ZScreen) From 1e288db5fce573170a9f38c0838e0de744e6edf7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Feb 2025 02:50:49 -0800 Subject: [PATCH 441/811] use new overlay isOverlayEnabled API --- agitation-rebalance.lua | 3 +-- gui/notes.lua | 3 +-- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/agitation-rebalance.lua b/agitation-rebalance.lua index 37ba7925f4..ae22593a67 100644 --- a/agitation-rebalance.lua +++ b/agitation-rebalance.lua @@ -729,8 +729,7 @@ local function print_status() for k,v in pairs(state.features) do print((' %15s: %s'):format(k, v)) end - print((' %15s: %s'):format('monitor', - overlay.get_state().config[WIDGET_NAME].enabled or 'false')) + print((' %15s: %s'):format('monitor', overlay.isOverlayEnabled(WIDGET_NAME) or 'false')) print() print('difficulty settings:') print((' Wilderness irritation minimum: %d (about %d tree(s) until initial attacks are possible)'):format( diff --git a/gui/notes.lua b/gui/notes.lua index 358af1de6c..642604ce68 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -335,8 +335,7 @@ function NotesScreen:onRenderFrame(dc, rect) end function NotesScreen:onAboutToShow() - local notes_overlay = overlay.get_state().config[OVERLAY_NAME] - if notes_overlay and not notes_overlay.enabled then + if overlay.isOverlayEnabled(OVERLAY_NAME) then self.should_disable_overlay = true overlay.overlay_command({'enable', 'notes.map_notes'}) end From d0daba029fdb215f0609facd5a4f65fffb0e3b86 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 22 Feb 2025 22:19:49 +0100 Subject: [PATCH 442/811] spectate.lua: fix individual stress levels * set a stress level instead of the whole table * place the list widget deeper than toggle buttons --- gui/spectate.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/spectate.lua b/gui/spectate.lua index 3c708be7a1..13f7fbd57f 100644 --- a/gui/spectate.lua +++ b/gui/spectate.lua @@ -53,7 +53,7 @@ local function create_toggle_button(frame, cfg_elem, hotkey, label, cfg_elem_key return ToggleLabel{ frame=frame, initial_option=spectate.get_config_elem(cfg_elem, cfg_elem_key), - on_change=function(val) dfhack.run_command('spectate', 'set', cfg_elem, tostring(val)) end, + on_change=function(val) dfhack.run_command('spectate', 'set', cfg_elem, cfg_elem_key, tostring(val)) end, key=hotkey, label=label, } @@ -151,7 +151,7 @@ local function create_stress_list(frame, colFollow, colHover) table.insert(choices, make_choice({{text=elem.text, pen=elem.pen}, ' ', elem.name}, tlFollow, tlHover)) end - table.insert(subviews, widgets.List{ + table.insert(subviews, 1, widgets.List{ frame={l=2}, on_submit=function(_, choice) choice.data.tlFollow:cycle() end, on_submit2=function(_, choice) choice.data.tlHover:cycle() end, From 72b32ea2f8fe13694a379de8e02880c8b682e0ef Mon Sep 17 00:00:00 2001 From: Nicholas McDaniel Date: Sat, 22 Feb 2025 21:03:35 -0500 Subject: [PATCH 443/811] Prevent flow_forbid from being set without liquid present --- changelog.txt | 1 + modtools/spawn-liquid.lua | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/changelog.txt b/changelog.txt index 4965da0d76..c632f6f75f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,6 +36,7 @@ Template for new versions: ## Fixes - `position`: support for adv mode look cursor +- `gui/liquids`: using the remove tool with magma selected will no longer create unexpected unpathable tiles ## Misc Improvements - `hide-tutorials`: handle tutorial popups for adventure mode diff --git a/modtools/spawn-liquid.lua b/modtools/spawn-liquid.lua index 419b0b4595..9a3d2417e8 100644 --- a/modtools/spawn-liquid.lua +++ b/modtools/spawn-liquid.lua @@ -21,6 +21,10 @@ function spawnLiquid(position, liquid_level, liquid_type) local map_block = dfhack.maps.getTileBlock(position) local tile = dfhack.maps.getTileFlags(position) + if liquid_level == 0 then + liquid_type = df.tile_liquid.Water + end + tile.flow_size = liquid_level or 3 tile.liquid_type = liquid_type tile.flow_forbid = liquid_type == df.tile_liquid.Magma or liquid_level >= 4 @@ -34,6 +38,8 @@ function spawnLiquid(position, liquid_level, liquid_type) z_level.update = true z_level.update_twice = true + df.global.world.reindex_pathfinding = true + resetTemperature(position) end From 8504e4469f72450e11d64a25ae75445e9bc7da11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Tue, 11 Feb 2025 21:56:36 +0100 Subject: [PATCH 444/811] Prepare journal for various context (text and cursor) sources --- gui/journal.lua | 42 ++++++-------------- internal/journal/journal_context.lua | 57 ++++++++++++++++++++++++++++ 2 files changed, 69 insertions(+), 30 deletions(-) create mode 100644 internal/journal/journal_context.lua diff --git a/gui/journal.lua b/gui/journal.lua index 20ce638f2f..de0c1daf09 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -7,6 +7,7 @@ local utils = require 'utils' local json = require 'json' local shifter = reqscript('internal/journal/shifter') local table_of_contents = reqscript('internal/journal/table_of_contents') +local journal_context = reqscript('internal/journal/journal_context') local RESIZE_MIN = {w=54, h=20} local TOC_RESIZE_MIN = {w=24} @@ -291,7 +292,11 @@ JournalScreen.ATTRS { } function JournalScreen:init() - local context = self:loadContext() + self.journal_context = journal_context.journal_context_factory( + self.save_prefix, + self.save_on_change + ) + local context = self.journal_context:load() self:addviews{ JournalWindow{ @@ -304,40 +309,17 @@ function JournalScreen:init() init_cursor=context.cursor[1], show_tutorial=context.show_tutorial or false, - on_text_change=self:callback('saveContext'), - on_cursor_change=self:callback('saveContext') + on_text_change=self:callback('onTextChange'), + on_cursor_change=self:callback('onTextChange') }, } end -function JournalScreen:loadContext() - local site_data = self.save_on_change and dfhack.persistent.getSiteData( - self.save_prefix .. JOURNAL_PERSIST_KEY - ) or {} - - if not site_data.text then - site_data.text={''} - site_data.show_tutorial = true - end - site_data.cursor = site_data.cursor or {#site_data.text[1] + 1} - - return site_data -end - -function JournalScreen:onTextChange(text) - self:saveContext(text) -end +function JournalScreen:onTextChange() + local text = self.subviews.journal_editor:getText() + local cursor = self.subviews.journal_editor:getCursor() -function JournalScreen:saveContext() - if self.save_on_change and dfhack.isWorldLoaded() then - local text = self.subviews.journal_editor:getText() - local cursor = self.subviews.journal_editor:getCursor() - - dfhack.persistent.saveSiteData( - self.save_prefix .. JOURNAL_PERSIST_KEY, - {text={text}, cursor={cursor}} - ) - end + self.journal_context:save(text, cursor) end function JournalScreen:onDismiss() diff --git a/internal/journal/journal_context.lua b/internal/journal/journal_context.lua new file mode 100644 index 0000000000..beea28b981 --- /dev/null +++ b/internal/journal/journal_context.lua @@ -0,0 +1,57 @@ +--@ module = true + +local widgets = require 'gui.widgets' + +local JOURNAL_PERSIST_KEY = 'journal' + +function journal_context_factory(save_prefix, save_on_change) + if not save_on_change then + return DummyJournalContext{} + elseif dfhack.world.isFortressMode() then + return FortJournalContext{save_prefix} + else + qerror('unsupported game mode') + end +end + +FortJournalContext = defclass(FortJournalContext) +FortJournalContext.ATTRS{ + save_prefix='' +} + +function get_fort_context_key(prefix) + return prefix .. JOURNAL_PERSIST_KEY +end + +function FortJournalContext:save(text, cursor) + if dfhack.isWorldLoaded() then + dfhack.persistent.saveSiteData( + get_fort_context_key(self.save_prefix), + {text={text}, cursor={cursor}} + ) + end +end + +function FortJournalContext:load() + if dfhack.isWorldLoaded() then + local site_data = dfhack.persistent.getSiteData( + get_fort_context_key(self.save_prefix) + ) or {} + + if not site_data.text then + site_data.text={''} + site_data.show_tutorial = true + end + site_data.cursor = site_data.cursor or {#site_data.text[1] + 1} + return site_data + end +end + +DummyJournalContext = defclass(DummyJournalContext) + +function DummyJournalContext:save(text, cursor) +end + +function DummyJournalContext:load() + return {text={''}, cursor={1}, show_tutorial=true} +end From b836ca298bfb1e05d9e9b9abd3cdb38c71ebcaad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 16 Feb 2025 09:22:52 +0100 Subject: [PATCH 445/811] Add journal context for adventurer game mode --- gui/journal.lua | 5 ++- internal/journal/journal_context.lua | 64 +++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 3 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index de0c1daf09..277e8ba2f3 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -327,8 +327,9 @@ function JournalScreen:onDismiss() end function main(options) - if not dfhack.isMapLoaded() or not dfhack.world.isFortressMode() then - qerror('journal requires a fortress map to be loaded') + if not dfhack.isMapLoaded() or (not dfhack.world.isFortressMode() + and not dfhack.world.isAdventureMode()) then + qerror('journal requires a fortress/adventure map to be loaded') end local save_layout = options and options.save_layout diff --git a/internal/journal/journal_context.lua b/internal/journal/journal_context.lua index beea28b981..61a3a86594 100644 --- a/internal/journal/journal_context.lua +++ b/internal/journal/journal_context.lua @@ -4,16 +4,24 @@ local widgets = require 'gui.widgets' local JOURNAL_PERSIST_KEY = 'journal' -function journal_context_factory(save_prefix, save_on_change) +function journal_context_factory(save_on_change, save_prefix) if not save_on_change then return DummyJournalContext{} elseif dfhack.world.isFortressMode() then return FortJournalContext{save_prefix} + elseif dfhack.world.isAdventureMode() then + return AdventurerJournalContext{ + save_prefix, + adventurer_id=dfhack.world.getAdventurer().id + } else qerror('unsupported game mode') end end + +-- Fortress Context -- + FortJournalContext = defclass(FortJournalContext) FortJournalContext.ATTRS{ save_prefix='' @@ -47,6 +55,19 @@ function FortJournalContext:load() end end +-- Dummy Context, no storage -- + +DummyJournalContext = defclass(DummyJournalContext) + +function DummyJournalContext:save(text, cursor) +end + +function DummyJournalContext:load() + return {text={''}, cursor={1}, show_tutorial=true} +end + +-- Dummy Context, no storage -- + DummyJournalContext = defclass(DummyJournalContext) function DummyJournalContext:save(text, cursor) @@ -55,3 +76,44 @@ end function DummyJournalContext:load() return {text={''}, cursor={1}, show_tutorial=true} end + +-- Adventure Context -- + +AdventurerJournalContext = defclass(AdventurerJournalContext) +AdventurerJournalContext.ATTRS{ + save_prefix='', + adventurer_id='' +} + +function get_adventurer_context_key(prefix, adventurer_id) + return string.format( + '%s%s:adventurer:%s', + prefix, + JOURNAL_PERSIST_KEY, + adventurer_id + ) +end + +function AdventurerJournalContext:save(text, cursor) + if dfhack.isWorldLoaded() then + dfhack.persistent.saveSiteData( + get_adventurer_context_key(self.save_prefix, self.adventurer_id), + {text={text}, cursor={cursor}} + ) + end +end + +function AdventurerJournalContext:load() + if dfhack.isWorldLoaded() then + local site_data = dfhack.persistent.getSiteData( + get_adventurer_context_key(self.save_prefix, self.adventurer_id) + ) or {} + + if not site_data.text then + site_data.text={''} + site_data.show_tutorial = true + end + site_data.cursor = site_data.cursor or {#site_data.text[1] + 1} + return site_data + end +end From a643f6b865f4cbe4935ace9ec868a501a76dd8b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 26 Feb 2025 07:39:09 +0100 Subject: [PATCH 446/811] Refactor journal contexts --- internal/journal/contexts/adventure.lua | 38 ++++++++ internal/journal/contexts/dummy.lua | 13 +++ internal/journal/contexts/fortress.lua | 34 +++++++ internal/journal/journal_context.lua | 116 +++--------------------- 4 files changed, 98 insertions(+), 103 deletions(-) create mode 100644 internal/journal/contexts/adventure.lua create mode 100644 internal/journal/contexts/dummy.lua create mode 100644 internal/journal/contexts/fortress.lua diff --git a/internal/journal/contexts/adventure.lua b/internal/journal/contexts/adventure.lua new file mode 100644 index 0000000000..8ae6d9e2fc --- /dev/null +++ b/internal/journal/contexts/adventure.lua @@ -0,0 +1,38 @@ +--@ module = true + +AdventurerJournalContext = defclass(AdventurerJournalContext) +AdventurerJournalContext.ATTRS{ + save_prefix='', +} + +function get_adventurer_context_key(prefix, adventurer_id) + return string.format( + '%sjournal:adventurer:%s', + prefix, + adventurer_id + ) +end + +function AdventurerJournalContext:save(text, cursor) + if dfhack.isWorldLoaded() then + dfhack.persistent.saveSiteData( + get_adventurer_context_key(self.save_prefix, self.adventurer_id), + {text={text}, cursor={cursor}} + ) + end +end + +function AdventurerJournalContext:load() + if dfhack.isWorldLoaded() then + local site_data = dfhack.persistent.getSiteData( + get_adventurer_context_key(self.save_prefix, self.adventurer_id) + ) or {} + + if not site_data.text then + site_data.text={''} + site_data.show_tutorial = true + end + site_data.cursor = site_data.cursor or {#site_data.text[1] + 1} + return site_data + end +end diff --git a/internal/journal/contexts/dummy.lua b/internal/journal/contexts/dummy.lua new file mode 100644 index 0000000000..50178bab1e --- /dev/null +++ b/internal/journal/contexts/dummy.lua @@ -0,0 +1,13 @@ +--@ module = true + +-- Dummy Context, no storage -- + +DummyJournalContext = defclass(DummyJournalContext) + +function DummyJournalContext:save(text, cursor) +end + +function DummyJournalContext:load() + return {text={''}, cursor={1}, show_tutorial=true} +end + diff --git a/internal/journal/contexts/fortress.lua b/internal/journal/contexts/fortress.lua new file mode 100644 index 0000000000..db1a64c434 --- /dev/null +++ b/internal/journal/contexts/fortress.lua @@ -0,0 +1,34 @@ +--@ module = true + +FortressJournalContext = defclass(FortressJournalContext) +FortressJournalContext.ATTRS{ + save_prefix='' +} + +function get_fort_context_key(prefix) + return prefix .. 'journal' +end + +function FortressJournalContext:save(text, cursor) + if dfhack.isWorldLoaded() then + dfhack.persistent.saveSiteData( + get_fort_context_key(self.save_prefix), + {text={text}, cursor={cursor}} + ) + end +end + +function FortressJournalContext:load() + if dfhack.isWorldLoaded() then + local site_data = dfhack.persistent.getSiteData( + get_fort_context_key(self.save_prefix) + ) or {} + + if not site_data.text then + site_data.text={''} + site_data.show_tutorial = true + end + site_data.cursor = site_data.cursor or {#site_data.text[1] + 1} + return site_data + end +end diff --git a/internal/journal/journal_context.lua b/internal/journal/journal_context.lua index 61a3a86594..e85dc70ec0 100644 --- a/internal/journal/journal_context.lua +++ b/internal/journal/journal_context.lua @@ -1,119 +1,29 @@ --@ module = true local widgets = require 'gui.widgets' - -local JOURNAL_PERSIST_KEY = 'journal' +local utils = require('utils') +local DummyJournalContext = reqscript('internal/journal/contexts/dummy') +local FortressJournalContext = reqscript('internal/journal/contexts/fortress') +local AdventurerJournalContext = reqscript('internal/journal/contexts/adventure') function journal_context_factory(save_on_change, save_prefix) if not save_on_change then return DummyJournalContext{} elseif dfhack.world.isFortressMode() then - return FortJournalContext{save_prefix} + return FortressJournalContext{save_prefix} elseif dfhack.world.isAdventureMode() then + local interactions = df.global.adventure.interactions + if #interactions.party_core_members == 0 then + qerror('Can not identify party core member') + end + + local adventurer_id = interactions.party_core_members[0] + return AdventurerJournalContext{ save_prefix, - adventurer_id=dfhack.world.getAdventurer().id + adventurer_id=adventurer_id } else qerror('unsupported game mode') end end - - --- Fortress Context -- - -FortJournalContext = defclass(FortJournalContext) -FortJournalContext.ATTRS{ - save_prefix='' -} - -function get_fort_context_key(prefix) - return prefix .. JOURNAL_PERSIST_KEY -end - -function FortJournalContext:save(text, cursor) - if dfhack.isWorldLoaded() then - dfhack.persistent.saveSiteData( - get_fort_context_key(self.save_prefix), - {text={text}, cursor={cursor}} - ) - end -end - -function FortJournalContext:load() - if dfhack.isWorldLoaded() then - local site_data = dfhack.persistent.getSiteData( - get_fort_context_key(self.save_prefix) - ) or {} - - if not site_data.text then - site_data.text={''} - site_data.show_tutorial = true - end - site_data.cursor = site_data.cursor or {#site_data.text[1] + 1} - return site_data - end -end - --- Dummy Context, no storage -- - -DummyJournalContext = defclass(DummyJournalContext) - -function DummyJournalContext:save(text, cursor) -end - -function DummyJournalContext:load() - return {text={''}, cursor={1}, show_tutorial=true} -end - --- Dummy Context, no storage -- - -DummyJournalContext = defclass(DummyJournalContext) - -function DummyJournalContext:save(text, cursor) -end - -function DummyJournalContext:load() - return {text={''}, cursor={1}, show_tutorial=true} -end - --- Adventure Context -- - -AdventurerJournalContext = defclass(AdventurerJournalContext) -AdventurerJournalContext.ATTRS{ - save_prefix='', - adventurer_id='' -} - -function get_adventurer_context_key(prefix, adventurer_id) - return string.format( - '%s%s:adventurer:%s', - prefix, - JOURNAL_PERSIST_KEY, - adventurer_id - ) -end - -function AdventurerJournalContext:save(text, cursor) - if dfhack.isWorldLoaded() then - dfhack.persistent.saveSiteData( - get_adventurer_context_key(self.save_prefix, self.adventurer_id), - {text={text}, cursor={cursor}} - ) - end -end - -function AdventurerJournalContext:load() - if dfhack.isWorldLoaded() then - local site_data = dfhack.persistent.getSiteData( - get_adventurer_context_key(self.save_prefix, self.adventurer_id) - ) or {} - - if not site_data.text then - site_data.text={''} - site_data.show_tutorial = true - end - site_data.cursor = site_data.cursor or {#site_data.text[1] + 1} - return site_data - end -end From ee5308b4156ac1527932cf5888868da0ab9caa45 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 26 Feb 2025 08:04:37 +0100 Subject: [PATCH 447/811] Make journal welcome text contextual (different for fort/adv) --- gui/journal.lua | 43 ++++++++----------------- internal/journal/contexts/adventure.lua | 35 ++++++++++++++++++-- internal/journal/contexts/dummy.lua | 21 ++++++++++-- internal/journal/contexts/fortress.lua | 34 +++++++++++++++++-- internal/journal/journal_context.lua | 9 +++--- 5 files changed, 101 insertions(+), 41 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 277e8ba2f3..30e12f7c67 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -12,29 +12,6 @@ local journal_context = reqscript('internal/journal/journal_context') local RESIZE_MIN = {w=54, h=20} local TOC_RESIZE_MIN = {w=24} -local JOURNAL_PERSIST_KEY = 'journal' - -local JOURNAL_WELCOME_COPY = [=[ -Welcome to gui/journal, the chronicler's tool for Dwarf Fortress! - -Here, you can carve out notes, sketch your grand designs, or record the history of your fortress. -The text you write here is saved together with your fort. - -For guidance on navigation and hotkeys, tap the ? button in the upper right corner. -Happy digging! -]=] - -local TOC_WELCOME_COPY = [=[ -Start a line with # symbols and a space to create a header. For example: - -# My section heading - -or - -## My section subheading - -Those headers will appear here, and you can click on them to jump to them in the text.]=] - journal_config = journal_config or json.open('dfhack-config/journal.json') JournalWindow = defclass(JournalWindow, widgets.Window) @@ -48,6 +25,9 @@ JournalWindow.ATTRS { save_layout=true, show_tutorial=false, + toc_welcome_copy=DEFAULT_NIL, + journal_welcome_copy=DEFAULT_NIL, + on_text_change=DEFAULT_NIL, on_cursor_change=DEFAULT_NIL, on_layout_change=DEFAULT_NIL @@ -77,7 +57,7 @@ function JournalWindow:init() widgets.WrappedLabel{ view_id='table_of_contents_tutorial', frame={l=0,t=0,r=0,b=3}, - text_to_wrap=TOC_WELCOME_COPY, + text_to_wrap=self.toc_welcome_copy or '', visible=false } } @@ -144,7 +124,7 @@ function JournalWindow:init() widgets.WrappedLabel{ view_id='journal_tutorial', frame={l=0,t=1,r=0,b=0}, - text_to_wrap=JOURNAL_WELCOME_COPY + text_to_wrap=self.journal_welcome_copy or '' } } end @@ -296,7 +276,7 @@ function JournalScreen:init() self.save_prefix, self.save_on_change ) - local context = self.journal_context:load() + local content = self.journal_context:load_content() self:addviews{ JournalWindow{ @@ -305,9 +285,12 @@ function JournalScreen:init() save_layout=self.save_layout, - init_text=context.text[1], - init_cursor=context.cursor[1], - show_tutorial=context.show_tutorial or false, + init_text=content.text[1], + init_cursor=content.cursor[1], + show_tutorial=content.show_tutorial or false, + + toc_welcome_copy=self.journal_context:tocWelcomeCopy(), + journal_welcome_copy=self.journal_context:welcomeCopy(), on_text_change=self:callback('onTextChange'), on_cursor_change=self:callback('onTextChange') @@ -319,7 +302,7 @@ function JournalScreen:onTextChange() local text = self.subviews.journal_editor:getText() local cursor = self.subviews.journal_editor:getCursor() - self.journal_context:save(text, cursor) + self.journal_context:save_content(text, cursor) end function JournalScreen:onDismiss() diff --git a/internal/journal/contexts/adventure.lua b/internal/journal/contexts/adventure.lua index 8ae6d9e2fc..5d8faffe34 100644 --- a/internal/journal/contexts/adventure.lua +++ b/internal/journal/contexts/adventure.lua @@ -1,8 +1,31 @@ --@ module = true +local JOURNAL_WELCOME_COPY = [=[ +Welcome to gui/journal, the adventurer's notebook for Dwarf Fortress! + +Here, you can jot down your travels, keep track of important places, or note anything worth remembering. +The text you write here is saved together with your adventurer. + +For guidance on navigation and hotkeys, tap the ? button in the upper right corner. +Safe travels! +]=] + +local TOC_WELCOME_COPY = [=[ +Start a line with # symbols and a space to create a header. For example: + +# My section heading + +or + +## My section subheading + +Those headers will appear here, and you can click on them to jump to them in the text.]=] + + AdventurerJournalContext = defclass(AdventurerJournalContext) AdventurerJournalContext.ATTRS{ save_prefix='', + adventurer_id=DEFAULT_NIL } function get_adventurer_context_key(prefix, adventurer_id) @@ -13,7 +36,7 @@ function get_adventurer_context_key(prefix, adventurer_id) ) end -function AdventurerJournalContext:save(text, cursor) +function AdventurerJournalContext:save_content(text, cursor) if dfhack.isWorldLoaded() then dfhack.persistent.saveSiteData( get_adventurer_context_key(self.save_prefix, self.adventurer_id), @@ -22,7 +45,7 @@ function AdventurerJournalContext:save(text, cursor) end end -function AdventurerJournalContext:load() +function AdventurerJournalContext:load_content() if dfhack.isWorldLoaded() then local site_data = dfhack.persistent.getSiteData( get_adventurer_context_key(self.save_prefix, self.adventurer_id) @@ -36,3 +59,11 @@ function AdventurerJournalContext:load() return site_data end end + +function AdventurerJournalContext:welcomeCopy() + return JOURNAL_WELCOME_COPY +end + +function AdventurerJournalContext:tocWelcomeCopy() + return TOC_WELCOME_COPY +end diff --git a/internal/journal/contexts/dummy.lua b/internal/journal/contexts/dummy.lua index 50178bab1e..aa3f55d338 100644 --- a/internal/journal/contexts/dummy.lua +++ b/internal/journal/contexts/dummy.lua @@ -1,13 +1,30 @@ --@ module = true +local JOURNAL_WELCOME_COPY = [=[ +Welcome to gui/journal. This is dummy context and it should be available only +in automatic tests. +]=] + +local TOC_WELCOME_COPY = [=[ +This is Table of Contenst test welcome copy +]=] + + -- Dummy Context, no storage -- DummyJournalContext = defclass(DummyJournalContext) -function DummyJournalContext:save(text, cursor) +function DummyJournalContext:save_content(text, cursor) end -function DummyJournalContext:load() +function DummyJournalContext:load_content() return {text={''}, cursor={1}, show_tutorial=true} end +function DummyJournalContext:welcomeCopy() + return JOURNAL_WELCOME_COPY +end + +function DummyJournalContext:tocWelcomeCopy() + return TOC_WELCOME_COPY +end diff --git a/internal/journal/contexts/fortress.lua b/internal/journal/contexts/fortress.lua index db1a64c434..c8e8f2a8a0 100644 --- a/internal/journal/contexts/fortress.lua +++ b/internal/journal/contexts/fortress.lua @@ -1,5 +1,27 @@ --@ module = true +local JOURNAL_WELCOME_COPY = [=[ +Welcome to gui/journal, the chronicler's tool for Dwarf Fortress! + +Here, you can carve out notes, sketch your grand designs, or record the history of your fortress. +The text you write here is saved together with your fort. + +For guidance on navigation and hotkeys, tap the ? button in the upper right corner. +Happy digging! +]=] + +local TOC_WELCOME_COPY = [=[ +Start a line with # symbols and a space to create a header. For example: + +# My section heading + +or + +## My section subheading + +Those headers will appear here, and you can click on them to jump to them in the text.]=] + + FortressJournalContext = defclass(FortressJournalContext) FortressJournalContext.ATTRS{ save_prefix='' @@ -9,7 +31,7 @@ function get_fort_context_key(prefix) return prefix .. 'journal' end -function FortressJournalContext:save(text, cursor) +function FortressJournalContext:save_content(text, cursor) if dfhack.isWorldLoaded() then dfhack.persistent.saveSiteData( get_fort_context_key(self.save_prefix), @@ -18,7 +40,7 @@ function FortressJournalContext:save(text, cursor) end end -function FortressJournalContext:load() +function FortressJournalContext:load_content() if dfhack.isWorldLoaded() then local site_data = dfhack.persistent.getSiteData( get_fort_context_key(self.save_prefix) @@ -32,3 +54,11 @@ function FortressJournalContext:load() return site_data end end + +function FortressJournalContext:welcomeCopy() + return JOURNAL_WELCOME_COPY +end + +function FortressJournalContext:tocWelcomeCopy() + return TOC_WELCOME_COPY +end diff --git a/internal/journal/journal_context.lua b/internal/journal/journal_context.lua index e85dc70ec0..a9b7485aaf 100644 --- a/internal/journal/journal_context.lua +++ b/internal/journal/journal_context.lua @@ -2,9 +2,9 @@ local widgets = require 'gui.widgets' local utils = require('utils') -local DummyJournalContext = reqscript('internal/journal/contexts/dummy') -local FortressJournalContext = reqscript('internal/journal/contexts/fortress') -local AdventurerJournalContext = reqscript('internal/journal/contexts/adventure') +local DummyJournalContext = reqscript('internal/journal/contexts/dummy').DummyJournalContext +local FortressJournalContext = reqscript('internal/journal/contexts/fortress').FortressJournalContext +local AdventurerJournalContext = reqscript('internal/journal/contexts/adventure').AdventurerJournalContext function journal_context_factory(save_on_change, save_prefix) if not save_on_change then @@ -13,12 +13,11 @@ function journal_context_factory(save_on_change, save_prefix) return FortressJournalContext{save_prefix} elseif dfhack.world.isAdventureMode() then local interactions = df.global.adventure.interactions - if #interactions.party_core_members == 0 then + if #interactions.party_core_members == 0 or interactions.party_core_members[0] == nil then qerror('Can not identify party core member') end local adventurer_id = interactions.party_core_members[0] - return AdventurerJournalContext{ save_prefix, adventurer_id=adventurer_id From 4c047c1d23fd9e0ba1eeb676db5765dc8b9dad26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 26 Feb 2025 08:20:20 +0100 Subject: [PATCH 448/811] Adjust journal help to adventurer support --- docs/gui/journal.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index 0063ff63ad..d773e2f863 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -6,11 +6,11 @@ gui/journal :tags: fort interface The `gui/journal` interface makes it easy to take notes and document -important details for the fortresses. +important details for your fortresses and adventurers. With this multi-line text editor, -you can keep track of your fortress's background story, goals, notable events, -and both short-term and long-term plans. +you can keep track of your fortress's/adventurer's background story, goals, +notable events, and both short-term and long-term plans. This is particularly useful when you need to take a longer break from the game. Having detailed notes makes it much easier to resume your game after From bafc23dc0017d0d738a3b5d03dad2167c752afe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 26 Feb 2025 18:17:40 +0100 Subject: [PATCH 449/811] Make gui/journal contexts testable --- gui/journal.lua | 14 +++++--- internal/journal/contexts/adventure.lua | 22 +++++++++---- internal/journal/contexts/dummy.lua | 17 +++------- internal/journal/contexts/fortress.lua | 8 +++++ internal/journal/journal_context.lua | 27 +++++++++++++--- test/gui/journal.lua | 43 +++++++++++++++++++------ 6 files changed, 92 insertions(+), 39 deletions(-) diff --git a/gui/journal.lua b/gui/journal.lua index 30e12f7c67..5010933392 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -14,6 +14,8 @@ local TOC_RESIZE_MIN = {w=24} journal_config = journal_config or json.open('dfhack-config/journal.json') +JOURNAL_CONTEXT_MODE = journal_context.JOURNAL_CONTEXT_MODE + JournalWindow = defclass(JournalWindow, widgets.Window) JournalWindow.ATTRS { frame_title='DF Journal', @@ -266,15 +268,15 @@ end JournalScreen = defclass(JournalScreen, gui.ZScreen) JournalScreen.ATTRS { focus_path='journal', - save_on_change=true, + context_mode=DEFAULT_NIL, save_layout=true, save_prefix='' } function JournalScreen:init() self.journal_context = journal_context.journal_context_factory( - self.save_prefix, - self.save_on_change + self.context_mode, + self.save_prefix ) local content = self.journal_context:load_content() @@ -316,12 +318,14 @@ function main(options) end local save_layout = options and options.save_layout - local save_on_change = options and options.save_on_change + local overrided_context_mode = options and options.context_mode + local context_mode = overrided_context_mode == nil and + journal_context.detect_journal_context_mode() or overrided_context_mode view = view and view:raise() or JournalScreen{ save_prefix=options and options.save_prefix or '', save_layout=save_layout == nil and true or save_layout, - save_on_change=save_on_change == nil and true or save_on_change, + context_mode=context_mode, }:show() end diff --git a/internal/journal/contexts/adventure.lua b/internal/journal/contexts/adventure.lua index 5d8faffe34..9957cbb615 100644 --- a/internal/journal/contexts/adventure.lua +++ b/internal/journal/contexts/adventure.lua @@ -38,7 +38,7 @@ end function AdventurerJournalContext:save_content(text, cursor) if dfhack.isWorldLoaded() then - dfhack.persistent.saveSiteData( + dfhack.persistent.saveWorldData( get_adventurer_context_key(self.save_prefix, self.adventurer_id), {text={text}, cursor={cursor}} ) @@ -47,16 +47,24 @@ end function AdventurerJournalContext:load_content() if dfhack.isWorldLoaded() then - local site_data = dfhack.persistent.getSiteData( + local world_data = dfhack.persistent.getWorldData( get_adventurer_context_key(self.save_prefix, self.adventurer_id) ) or {} - if not site_data.text then - site_data.text={''} - site_data.show_tutorial = true + if not world_data.text then + world_data.text={''} + world_data.show_tutorial = true end - site_data.cursor = site_data.cursor or {#site_data.text[1] + 1} - return site_data + world_data.cursor = world_data.cursor or {#world_data.text[1] + 1} + return world_data + end +end + +function AdventurerJournalContext:delete_content() + if dfhack.isWorldLoaded() then + dfhack.persistent.deleteWorldData( + get_adventurer_context_key(self.save_prefix, self.adventurer_id) + ) end end diff --git a/internal/journal/contexts/dummy.lua b/internal/journal/contexts/dummy.lua index aa3f55d338..1993bb770b 100644 --- a/internal/journal/contexts/dummy.lua +++ b/internal/journal/contexts/dummy.lua @@ -1,15 +1,5 @@ --@ module = true -local JOURNAL_WELCOME_COPY = [=[ -Welcome to gui/journal. This is dummy context and it should be available only -in automatic tests. -]=] - -local TOC_WELCOME_COPY = [=[ -This is Table of Contenst test welcome copy -]=] - - -- Dummy Context, no storage -- DummyJournalContext = defclass(DummyJournalContext) @@ -21,10 +11,13 @@ function DummyJournalContext:load_content() return {text={''}, cursor={1}, show_tutorial=true} end +function DummyJournalContext:delete_content() +end + function DummyJournalContext:welcomeCopy() - return JOURNAL_WELCOME_COPY + return '' end function DummyJournalContext:tocWelcomeCopy() - return TOC_WELCOME_COPY + return '' end diff --git a/internal/journal/contexts/fortress.lua b/internal/journal/contexts/fortress.lua index c8e8f2a8a0..46a28f568c 100644 --- a/internal/journal/contexts/fortress.lua +++ b/internal/journal/contexts/fortress.lua @@ -55,6 +55,14 @@ function FortressJournalContext:load_content() end end +function FortressJournalContext:delete_content() + if dfhack.isWorldLoaded() then + dfhack.persistent.deleteSiteData( + get_fort_context_key(self.save_prefix) + ) + end +end + function FortressJournalContext:welcomeCopy() return JOURNAL_WELCOME_COPY end diff --git a/internal/journal/journal_context.lua b/internal/journal/journal_context.lua index a9b7485aaf..989e10f06c 100644 --- a/internal/journal/journal_context.lua +++ b/internal/journal/journal_context.lua @@ -6,22 +6,39 @@ local DummyJournalContext = reqscript('internal/journal/contexts/dummy').DummyJo local FortressJournalContext = reqscript('internal/journal/contexts/fortress').FortressJournalContext local AdventurerJournalContext = reqscript('internal/journal/contexts/adventure').AdventurerJournalContext -function journal_context_factory(save_on_change, save_prefix) - if not save_on_change then - return DummyJournalContext{} - elseif dfhack.world.isFortressMode() then - return FortressJournalContext{save_prefix} +JOURNAL_CONTEXT_MODE = { + FORTRESS='fortress', + ADVENTURE='adventure', + DUMMY='dummy' +} + +function detect_journal_context_mode() + if dfhack.world.isFortressMode() then + return JOURNAL_CONTEXT_MODE.FORTRESS elseif dfhack.world.isAdventureMode() then + return JOURNAL_CONTEXT_MODE.ADVENTURE + else + qerror('unsupported game mode') + end +end + +function journal_context_factory(journal_context_mode, save_prefix) + if journal_context_mode == JOURNAL_CONTEXT_MODE.FORTRESS then + return FortressJournalContext{save_prefix} + elseif journal_context_mode == JOURNAL_CONTEXT_MODE.ADVENTURE then local interactions = df.global.adventure.interactions if #interactions.party_core_members == 0 or interactions.party_core_members[0] == nil then qerror('Can not identify party core member') end local adventurer_id = interactions.party_core_members[0] + return AdventurerJournalContext{ save_prefix, adventurer_id=adventurer_id } + elseif journal_context_mode == JOURNAL_CONTEXT_MODE.DUMMY then + return DummyJournalContext{} else qerror('unsupported game mode') end diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 19975a7379..2ce3288886 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -1,5 +1,6 @@ local gui = require('gui') local gui_journal = reqscript('gui/journal') +local journal_context = reqscript('internal/journal/journal_context') config = { target = 'gui/journal', @@ -75,8 +76,8 @@ local function arrange_empty_journal(options) options = options or {} gui_journal.main({ - save_prefix='test:', - save_on_change=options.save_on_change or false, + save_prefix=options.save_prefix or 'test:', + context_mode=options.context_mode or gui_journal.JOURNAL_CONTEXT_MODE.DUMMY, save_layout=options.allow_layout_restore or false, }) @@ -98,9 +99,9 @@ local function arrange_empty_journal(options) local text_area = journal_window.subviews.journal_editor.text_area text_area.enable_cursor_blink = false - if not options.save_on_change then - text_area:setText('') - end + -- if not options.save_on_change then + -- text_area:setText('') + -- end if not options.allow_layout_restore then local toc_panel = journal_window.subviews.table_of_contents_panel @@ -202,7 +203,10 @@ function test.restore_layout() end function test.restore_text_between_sessions() - local journal, text_area = arrange_empty_journal({w=80,save_on_change=true}) + local journal, text_area = arrange_empty_journal({ + w=80, + context_mode=gui_journal.JOURNAL_CONTEXT_MODE.FORTRESS + }) simulate_input_keys('CUSTOM_CTRL_A') simulate_input_keys('CUSTOM_DELETE') @@ -224,7 +228,10 @@ function test.restore_text_between_sessions() journal:dismiss() - journal, text_area = arrange_empty_journal({w=80, save_on_change=true}) + journal, text_area = arrange_empty_journal({ + w=80, + context_mode=gui_journal.JOURNAL_CONTEXT_MODE.FORTRESS + }) expect.eq(read_rendered_text(text_area), table.concat({ '60: Lorem ipsum dolor sit amet, consectetur adipiscing elit.', @@ -600,11 +607,27 @@ function test.table_of_contents_keyboard_navigation() journal:dismiss() end -function test.show_tutorials_on_first_use() - local journal, text_area, journal_window = arrange_empty_journal({w=65}) +function test.show_fortress_tutorials_on_first_use() + local save_prefix = 'test:' + local context = journal_context.journal_context_factory( + gui_journal.JOURNAL_CONTEXT_MODE.FORTRESS, + save_prefix + ) + -- reset saved data + context:delete_content() + + local journal, text_area, journal_window = arrange_empty_journal({ + w=125, + context_mode=gui_journal.JOURNAL_CONTEXT_MODE.FORTRESS, + save_prefix=save_prefix + }) + simulate_input_keys('CUSTOM_CTRL_O') - expect.str_find('Welcome to gui/journal', read_rendered_text(text_area)); + expect.str_find( + "Welcome to gui/journal, the chronicler's tool for Dwarf Fortress!", + read_rendered_text(text_area) + ); simulate_input_text(' ') From 68c2ebbcb5a158f0121b5b238aba6494a5b52236 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 26 Feb 2025 18:32:40 +0100 Subject: [PATCH 450/811] Add adventure mode journal info to changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index b4e7bddd2d..69947583bc 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: ## New Features - `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys +- `gui/journal`: now working in adventure mode ## Fixes - `position`: support for adv mode look cursor From 1feabe8769bc4475af2dfdeae071a61680f9ea67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 26 Feb 2025 19:22:08 +0100 Subject: [PATCH 451/811] Remove redundant comment --- test/gui/journal.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 2ce3288886..8571937157 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -99,9 +99,6 @@ local function arrange_empty_journal(options) local text_area = journal_window.subviews.journal_editor.text_area text_area.enable_cursor_blink = false - -- if not options.save_on_change then - -- text_area:setText('') - -- end if not options.allow_layout_restore then local toc_panel = journal_window.subviews.table_of_contents_panel From ca84f9c696389e308278ef633df2018e9241efa7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 10:27:43 -0800 Subject: [PATCH 452/811] toggle vanilla dimensions flag on overlay toggle that is, ensure vanilla tooltip is hidden when ours is enabled --- changelog.txt | 1 + docs/gui/design.rst | 22 ++++++++++++++-------- gui/design.lua | 8 ++++++++ 3 files changed, 23 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index b4e7bddd2d..1181c96810 100644 --- a/changelog.txt +++ b/changelog.txt @@ -50,6 +50,7 @@ Template for new versions: - `gui/sitemap`: shift click to start following the selected unit or artifact - `prioritize`: when prioritizing jobs of a specified type, also output how many of those jobs were already prioritized before you ran the command - `prioritize`: don't include already-prioritized jobs in the output of ``prioritize -j`` +- `gui/design`: only display vanilla dimensions tooltip if the DFHack dimensions tooltip is disabled ## Removed diff --git a/docs/gui/design.rst b/docs/gui/design.rst index 5518af6c9c..2c80170da1 100644 --- a/docs/gui/design.rst +++ b/docs/gui/design.rst @@ -7,7 +7,8 @@ gui/design :tags: fort design productivity interface map This tool provides a point and click interface to make designating shapes -and patterns easier. Supports both digging designations and placing constructions. +and patterns easier. Supports both digging designations and placing +constructions. Usage ----- @@ -19,18 +20,23 @@ Usage Overlay ------- -This tool also provides two overlays that are managed by the `overlay` framework. +This tool also provides two overlays that are managed by the `overlay` +framework. dimensions ~~~~~~~~~~ -The ``gui/design.dimensions`` overlay shows the selected dimensions when designating -with vanilla tools, for example when painting a burrow or designating digging. -The dimensions show up in a tooltip that follows the mouse cursor. +The ``gui/design.dimensions`` overlay shows the selected dimensions when +designating with vanilla tools, for example when painting a burrow or +designating digging. The dimensions show up in a tooltip that follows the mouse +cursor. + +When this overlay is enabled, the vanilla dimensions display will be hidden. +When this overlay is disabled, the vanilla dimensions display will be unhidden. rightclick ~~~~~~~~~~ -The ``gui/design.rightclick`` overlay prevents the right mouse button and other keys -bound to "Leave screen" from exiting out of designation mode when drawing a box with -vanilla tools, instead making it cancel the designation first. +The ``gui/design.rightclick`` overlay prevents the right mouse button and other +keys bound to "Leave screen" from exiting out of designation mode when drawing +a box with vanilla tools, instead making it cancel the designation first. diff --git a/gui/design.lua b/gui/design.lua index 2279ec005a..103ab59c8e 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -122,6 +122,14 @@ function DimensionsOverlay:preUpdateLayout(parent_rect) self.frame.h = parent_rect.height end +function DimensionsOverlay:overlay_onenable() + df.global.d_init.display.flags.SHOW_RECTANGLE_DIMENSIONS = false +end + +function DimensionsOverlay:overlay_ondisable() + df.global.d_init.display.flags.SHOW_RECTANGLE_DIMENSIONS = true +end + --- --- RightClickOverlay --- From 108e5e9a3774a57758404704c706361d2b1745d9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 11:20:48 -0800 Subject: [PATCH 453/811] changelog editing pass --- changelog.txt | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/changelog.txt b/changelog.txt index 1181c96810..7d89fcd860 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,7 +29,7 @@ Template for new versions: ## New Tools - `devel/export-map`: export map tile data to a JSON file - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk -- `gui/spectate`: interactive UI for configuring `spectate` +- `gui/spectate`: interactive UI for configuring new `spectate` features - `gui/notes`: UI for adding and managing notes attached to tiles on the map ## New Features @@ -46,7 +46,9 @@ Template for new versions: - `gui/notify`: save reminder now appears in adventure mode - `gui/notify`: save reminder changes color to yellow at 30 minutes and to orange at 60 minutes - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete -- `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. +- `position`: add adventurer tile position +- `position`: add global site position +- `position`: when a tile is selected, display relevant map block and intra-block offset - `gui/sitemap`: shift click to start following the selected unit or artifact - `prioritize`: when prioritizing jobs of a specified type, also output how many of those jobs were already prioritized before you ran the command - `prioritize`: don't include already-prioritized jobs in the output of ``prioritize -j`` @@ -98,8 +100,6 @@ Template for new versions: - `fix/wildlife`: don't vaporize stuck wildlife that is onscreen -- kill them instead (as if they died from old age) - `gui/sitemap`: show primary group affiliation for visitors and invaders (e.g. civilization name or performance troupe) -# 50.15-r1 - # 50.14-r2 ## New Tools @@ -696,8 +696,6 @@ Template for new versions: ## Removed - `gui/automelt`: replaced by an overlay panel that appears when you click on a stockpile -# 50.08-r3 - # 50.08-r2 ## New Scripts @@ -746,8 +744,6 @@ Template for new versions: # 50.07-r1 -## New Scripts - ## Fixes -@ `caravan`: fix trade good list sometimes disappearing when you collapse a bin -@ `gui/gm-editor`: no longer nudges last open window when opening a new one @@ -760,8 +756,6 @@ Template for new versions: - `prioritize`: revise and simplify the default list of prioritized jobs -- be sure to tell us if your forts are running noticeably better (or worse!) -@ `gui/control-panel`: add `faststart` to the system services -## Removed - # 50.07-beta2 ## New Scripts From e0eea952b0b822a1ec5f3510c5bb4020f8ad7170 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 11:46:02 -0800 Subject: [PATCH 454/811] only offer the spectate mode toggle in fort mode --- gui/spectate.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/spectate.lua b/gui/spectate.lua index 13f7fbd57f..416c48e94b 100644 --- a/gui/spectate.lua +++ b/gui/spectate.lua @@ -188,6 +188,7 @@ function Spectate:init() on_change=function(val) dfhack.run_command(val and 'enable' or 'disable', 'spectate') end, key='CUSTOM_ALT_E', label='Spectate mode ', + enabled=dfhack.world.isFortressMode, }, create_numeric_edit_field({t=4}, 'follow-seconds', 'CUSTOM_ALT_F', 'Switch target (sec): '), create_toggle_button({t=6}, 'auto-unpause', 'CUSTOM_ALT_U', rpad('Auto unpause', lWidth)), From e927734288e1676ccb6f25b96228719b79b48f13 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 28 Feb 2025 12:41:24 -0800 Subject: [PATCH 455/811] fix logic for notes overlay autoshow --- gui/notes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/notes.lua b/gui/notes.lua index 642604ce68..47b072887e 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -335,7 +335,7 @@ function NotesScreen:onRenderFrame(dc, rect) end function NotesScreen:onAboutToShow() - if overlay.isOverlayEnabled(OVERLAY_NAME) then + if not overlay.isOverlayEnabled(OVERLAY_NAME) then self.should_disable_overlay = true overlay.overlay_command({'enable', 'notes.map_notes'}) end From 59f1f8e671e99a56a5b54e346a8b78ed1d2a5505 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 16 Feb 2025 19:36:13 +0100 Subject: [PATCH 456/811] Add tests for notes overlay script --- internal/notes/note_manager.lua | 9 +- notes.lua | 16 +-- test/gui/journal.lua | 2 - test/overlay/notes.lua | 204 ++++++++++++++++++++++++++++++++ 4 files changed, 217 insertions(+), 14 deletions(-) create mode 100644 test/overlay/notes.lua diff --git a/internal/notes/note_manager.lua b/internal/notes/note_manager.lua index 6ba76555be..56342abb11 100644 --- a/internal/notes/note_manager.lua +++ b/internal/notes/note_manager.lua @@ -39,7 +39,6 @@ function NoteManager:init() frame={t=1,h=3}, frame_style=gui.FRAME_INTERIOR, init_text=self.note and self.note.point.name or '', - -- init_cursor=self.note and #self.note.point.name + 1 or 1, one_line_mode=true }, widgets.HotkeyLabel { @@ -97,11 +96,11 @@ function NoteManager:init() end function NoteManager:setNotePos(note_pos) - self.notes_pos = note_pos + self.note_pos = note_pos end function NoteManager:createNote() - local cursor_pos = self.notes_pos or guidm.getCursorPos() + local cursor_pos = self.note_pos or guidm.getCursorPos() if cursor_pos == nil then dfhack.printerr('Enable keyboard cursor to add a note.') return @@ -150,8 +149,8 @@ function NoteManager:saveNote() self.note.point.name = name self.note.point.comment = comment - if self.notes_pos then - self.note.pos=self.notes_pos + if self.note_pos then + self.note.pos=self.note_pos end if self.on_update then diff --git a/notes.lua b/notes.lua index f9cd2a854c..84244955de 100644 --- a/notes.lua +++ b/notes.lua @@ -4,12 +4,14 @@ local overlay = require('plugins.overlay') local guidm = require('gui.dwarfmode') local note_manager = reqscript('internal/notes/note_manager') -local green_pin = dfhack.textures.loadTileset( - 'hack/data/art/note_green_pin_map.png', - 32, - 32, - true -) +textures = { + green_pin = dfhack.textures.loadTileset( + 'hack/data/art/note_green_pin_map.png', + 32, + 32, + true + ) +} NotesOverlay = defclass(NotesOverlay, overlay.OverlayWidget) NotesOverlay.ATTRS{ @@ -113,7 +115,7 @@ function NotesOverlay:onRenderFrame(dc) dc:map(true) - local texpos = dfhack.textures.getTexposByHandle(green_pin[1]) + local texpos = dfhack.textures.getTexposByHandle(textures.green_pin[1]) dc:pen({fg=COLOR_BLACK, bg=COLOR_LIGHTCYAN, tile=texpos}) for _, note in pairs(self.visible_notes) do diff --git a/test/gui/journal.lua b/test/gui/journal.lua index 19975a7379..3ca7450c12 100644 --- a/test/gui/journal.lua +++ b/test/gui/journal.lua @@ -6,8 +6,6 @@ config = { mode = 'fortress' } -local df_major_version = tonumber(dfhack.getCompiledDFVersion():match('%d+')) - local function simulate_input_keys(...) local keys = {...} for _,key in ipairs(keys) do diff --git a/test/overlay/notes.lua b/test/overlay/notes.lua new file mode 100644 index 0000000000..b2dc4f7a5b --- /dev/null +++ b/test/overlay/notes.lua @@ -0,0 +1,204 @@ +local gui = require('gui') +local overlay = require('plugins.overlay') +local guidm = require('gui.dwarfmode') +local utils = require('utils') +local notes_textures = reqscript('notes').textures + +local waypoints = df.global.plotinfo.waypoints +local map_points = df.global.plotinfo.waypoints.points + +config = { + target = 'notes', + mode = 'fortress' +} + +local map_points_backup = nil + +local function install_notes_overlay(options) + options = options or {} + + map_points_backup = utils.clone(map_points) + map_points:resize(0) + + overlay.rescan() + overlay.overlay_command({'enable', 'notes.map_notes'}) + -- if overlay + local overlay_state = overlay.get_state() + if not overlay_state.config['notes.map_notes'].enabled then + qerror('can not enable notes.map_notes overlay') + end + + return overlay_state.db['notes.map_notes'].widget +end + +local function reload_notes() + overlay.overlay_command({'trigger', 'notes.map_notes'}) +end + +local function cleanup(notes_overlay) + if notes_overlay.note_manager then + notes_overlay.note_manager:dismiss() + end + + df.global.plotinfo.waypoints.points:resize(#map_points_backup) + for ind, map_point in ipairs(map_points_backup) do + df.global.plotinfo.waypoints.points[ind - 1] = map_point + end + map_points_backup = nil + + reload_notes() +end + +local function add_note(notes_overlay, pos, name, comment) + df.global.cursor = copyall(pos) + + local cmd_result = overlay.overlay_command({ + 'trigger', 'notes.map_notes', 'add' + }) + + notes_overlay.note_manager.subviews.name:setText(name) + notes_overlay.note_manager.subviews.comment:setText(comment) + + gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'CUSTOM_CTRL_ENTER') +end + + +function test.load_notes_overlay() + local notes_overlay = install_notes_overlay() + expect.ne(notes_overlay, nil) + cleanup(notes_overlay) +end + +function test.trigger_add_new_note_modal() + local notes_overlay = install_notes_overlay() + + local cmd_result = overlay.overlay_command({ + 'trigger', 'notes.map_notes', 'add' + }) + + expect.eq(cmd_result, true) + expect.ne(notes_overlay.note_manager, nil) + expect.eq(notes_overlay.note_manager.visible, true) + + cleanup(notes_overlay) +end + +function test.render_existing_notes() + local notes_overlay = install_notes_overlay() + + local pos_1 = {x=10, y=20, z=0} + local pos_2 = {x=10, y=20, z=0} + local pos_3 = {x=10, y=20, z=0} + + add_note(notes_overlay, pos_1, 'note 1', 'first note') + add_note(notes_overlay, pos_2, 'note 2', 'second note') + add_note(notes_overlay, pos_3, 'note 3', 'last note') + + reload_notes() + + local viewport = guidm.Viewport.get() + + local pin_textpos = dfhack.textures.getTexposByHandle( + notes_textures.green_pin[1] + ) + + for _, pos in ipairs({pos_1, pos_2, pos_3}) do + dfhack.gui.revealInDwarfmodeMap(pos) + + -- TODO: find better way to wait for overlay re-render + delay(10) + + local screen_pos = viewport:tileToScreen(pos) + local pen = dfhack.screen.readTile(screen_pos.x, screen_pos.y, true) + expect.eq(pen and pen.tile, pin_textpos) + end + + cleanup(notes_overlay) +end + +function test.edit_clicked_note() + local notes_overlay = install_notes_overlay() + + local pos = {x=10, y=20, z=0} + add_note(notes_overlay, pos, 'note 1', 'note to edit') + add_note(notes_overlay, {x=20, y=10, z=2}, 'note 2', 'other note') + add_note(notes_overlay, {x=0, y=10, z=5}, 'note 3', 'another note') + + reload_notes() + dfhack.screen.invalidate() + dfhack.gui.revealInDwarfmodeMap(pos) + + -- TODO: find better way to wait for overlay re-render + delay(10) + + notes_overlay:updateLayout() + + local viewport = guidm.Viewport.get() + local screen_pos = viewport:tileToScreen(pos) + + local rect = gui.ViewRect{rect=notes_overlay.frame_rect} + + -- should not be a test function to map screen tile to mouse pos? + df.global.gps.precise_mouse_x = screen_pos.x * df.global.gps.viewport_zoom_factor / 4 + df.global.gps.precise_mouse_y = screen_pos.y * df.global.gps.viewport_zoom_factor / 4 + + local screen = dfhack.gui.getCurViewscreen(true) + gui.simulateInput(screen, { + _MOUSE_L=true, + }) + + local note_manager = notes_overlay.note_manager + expect.ne(note_manager, nil) + + expect.eq(note_manager.subviews.name:getText(), 'note 1') + expect.eq(note_manager.subviews.comment:getText(), 'note to edit') + + note_manager.subviews.name:setText('edited note 1') + note_manager.subviews.comment:setText('edited comment') + + gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'CUSTOM_CTRL_ENTER') + + expect.eq(map_points[0].name, 'edited note 1') + expect.eq(map_points[0].comment, 'edited comment') + + cleanup(notes_overlay) +end + +function test.delete_clicked_note() + local notes_overlay = install_notes_overlay() + + local pos = {x=10, y=20, z=0} + add_note(notes_overlay, {x=20, y=10, z=2}, 'note 1', 'note to edit') + add_note(notes_overlay, pos, 'note 2', 'other note') + add_note(notes_overlay, {x=0, y=10, z=5}, 'note 3', 'another note') + + reload_notes() + dfhack.screen.invalidate() + dfhack.gui.revealInDwarfmodeMap(pos) + + notes_overlay:updateLayout() + + local viewport = guidm.Viewport.get() + local screen_pos = viewport:tileToScreen(pos) + + local rect = gui.ViewRect{rect=notes_overlay.frame_rect} + + -- should not be a test function to map screen tile to mouse pos? + df.global.gps.precise_mouse_x = screen_pos.x * df.global.gps.viewport_zoom_factor / 4 + df.global.gps.precise_mouse_y = screen_pos.y * df.global.gps.viewport_zoom_factor / 4 + + local screen = dfhack.gui.getCurViewscreen(true) + gui.simulateInput(screen, { + _MOUSE_L=true, + }) + + expect.eq(#map_points, 3) + + gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'CUSTOM_CTRL_D') + + expect.eq(#map_points, 2) + expect.eq(map_points[0].name, 'note 1') + expect.eq(map_points[1].name, 'note 3') + + cleanup(notes_overlay) +end From 4c98e7e6cc7735c695b45fc925f3cabe4b8d0654 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Mon, 17 Feb 2025 18:51:19 +0100 Subject: [PATCH 457/811] Make notes script tests only test on current visible screen We do not currently have an obvious way to center map on notes in tests and force-render map. Without it, we can test reliably only on current visible screen --- test/overlay/notes.lua | 66 ++++++++++++++++++++++-------------------- 1 file changed, 35 insertions(+), 31 deletions(-) diff --git a/test/overlay/notes.lua b/test/overlay/notes.lua index b2dc4f7a5b..704ceba52e 100644 --- a/test/overlay/notes.lua +++ b/test/overlay/notes.lua @@ -86,9 +86,14 @@ end function test.render_existing_notes() local notes_overlay = install_notes_overlay() - local pos_1 = {x=10, y=20, z=0} - local pos_2 = {x=10, y=20, z=0} - local pos_3 = {x=10, y=20, z=0} + local viewport = guidm.Viewport.get() + + local half_x = math.floor((viewport.x1 + viewport.x2) / 2) + local half_y = math.floor((viewport.y1 + viewport.y2) / 2) + + local pos_1 = {x=half_x, y=viewport.y1, z=viewport.z} + local pos_2 = {x=viewport.x1, y=half_y, z=viewport.z} + local pos_3 = {x=half_x, y=half_y, z=viewport.z} add_note(notes_overlay, pos_1, 'note 1', 'first note') add_note(notes_overlay, pos_2, 'note 2', 'second note') @@ -96,17 +101,12 @@ function test.render_existing_notes() reload_notes() - local viewport = guidm.Viewport.get() - local pin_textpos = dfhack.textures.getTexposByHandle( notes_textures.green_pin[1] ) for _, pos in ipairs({pos_1, pos_2, pos_3}) do - dfhack.gui.revealInDwarfmodeMap(pos) - - -- TODO: find better way to wait for overlay re-render - delay(10) + notes_overlay:render(gui.Painter.new()) local screen_pos = viewport:tileToScreen(pos) local pen = dfhack.screen.readTile(screen_pos.x, screen_pos.y, true) @@ -119,22 +119,22 @@ end function test.edit_clicked_note() local notes_overlay = install_notes_overlay() - local pos = {x=10, y=20, z=0} - add_note(notes_overlay, pos, 'note 1', 'note to edit') - add_note(notes_overlay, {x=20, y=10, z=2}, 'note 2', 'other note') - add_note(notes_overlay, {x=0, y=10, z=5}, 'note 3', 'another note') + local viewport = guidm.Viewport.get() - reload_notes() - dfhack.screen.invalidate() - dfhack.gui.revealInDwarfmodeMap(pos) + local half_x = math.floor((viewport.x1 + viewport.x2) / 2) + local half_y = math.floor((viewport.y1 + viewport.y2) / 2) - -- TODO: find better way to wait for overlay re-render - delay(10) + local pos_1 = {x=half_x, y=viewport.y1, z=viewport.z} + local pos_2 = {x=viewport.x1, y=half_y, z=viewport.z} + local pos_3 = {x=half_x, y=half_y, z=viewport.z} - notes_overlay:updateLayout() + add_note(notes_overlay, pos_1, 'note 1', 'note to edit') + add_note(notes_overlay, pos_2, 'note 2', 'other note') + add_note(notes_overlay, pos_3, 'note 3', 'another note') - local viewport = guidm.Viewport.get() - local screen_pos = viewport:tileToScreen(pos) + reload_notes() + + local screen_pos = viewport:tileToScreen(pos_1) local rect = gui.ViewRect{rect=notes_overlay.frame_rect} @@ -167,19 +167,23 @@ end function test.delete_clicked_note() local notes_overlay = install_notes_overlay() - local pos = {x=10, y=20, z=0} - add_note(notes_overlay, {x=20, y=10, z=2}, 'note 1', 'note to edit') - add_note(notes_overlay, pos, 'note 2', 'other note') - add_note(notes_overlay, {x=0, y=10, z=5}, 'note 3', 'another note') - reload_notes() - dfhack.screen.invalidate() - dfhack.gui.revealInDwarfmodeMap(pos) + local viewport = guidm.Viewport.get() - notes_overlay:updateLayout() + local half_x = math.floor((viewport.x1 + viewport.x2) / 2) + local half_y = math.floor((viewport.y1 + viewport.y2) / 2) - local viewport = guidm.Viewport.get() - local screen_pos = viewport:tileToScreen(pos) + local pos_1 = {x=half_x, y=viewport.y1, z=viewport.z} + local pos_2 = {x=viewport.x1, y=half_y, z=viewport.z} + local pos_3 = {x=half_x, y=half_y, z=viewport.z} + + add_note(notes_overlay, pos_1, 'note 1', 'note to edit') + add_note(notes_overlay, pos_2, 'note 2', 'other note') + add_note(notes_overlay, pos_3, 'note 3', 'another note') + + reload_notes() + + local screen_pos = viewport:tileToScreen(pos_2) local rect = gui.ViewRect{rect=notes_overlay.frame_rect} From 5fb75bbf764ec6111455b73d12c0dca6815bff36 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Mon, 17 Feb 2025 19:15:03 +0100 Subject: [PATCH 458/811] Add basic tests for gui/notes widget --- gui/notes.lua | 2 +- test/gui/notes.lua | 86 ++++++++++++++++++++++++++++++++++++++++++ test/overlay/notes.lua | 2 - 3 files changed, 87 insertions(+), 3 deletions(-) create mode 100644 test/gui/notes.lua diff --git a/gui/notes.lua b/gui/notes.lua index 47b072887e..2e3a80f484 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -348,7 +348,7 @@ function NotesScreen:onDismiss() view = nil end -function main(options) +function main() if not dfhack.isMapLoaded() or not dfhack.world.isFortressMode() then qerror('notes requires a fortress map to be loaded') end diff --git a/test/gui/notes.lua b/test/gui/notes.lua new file mode 100644 index 0000000000..941faf5e80 --- /dev/null +++ b/test/gui/notes.lua @@ -0,0 +1,86 @@ +local gui = require('gui') +local gui_notes = reqscript('gui/notes') +local utils = require('utils') + +-- local guidm = require('gui.dwarfmode') + +config = { + target = 'gui/notes', + mode = 'fortress' +} + +local waypoints = df.global.plotinfo.waypoints +local map_points = df.global.plotinfo.waypoints.points + +local map_points_backup = nil + +local function arrange_notes(notes) + map_points_backup = utils.clone(map_points) + map_points:resize(0) + + for _, note in ipairs(notes or {}) do + map_points:insert("#", { + new=true, + + id = waypoints.next_point_id, + tile=88, + fg_color=7, + bg_color=0, + name=note.name, + comment=note.comment, + pos=note.pos + }) + end +end + +local function arrange_gui_notes(options) + options = options or {} + + arrange_notes(options.notes) + + gui_notes.main() + + local gui_notes = gui_notes.view + + gui_notes:updateLayout() + gui_notes:onRender() + + return gui_notes +end + +local function cleanup(gui_notes) + gui_notes:dismiss() + + df.global.plotinfo.waypoints.points:resize(#map_points_backup) + for ind, map_point in ipairs(map_points_backup) do + df.global.plotinfo.waypoints.points[ind - 1] = map_point + end + map_points_backup = nil +end + +function test.load_gui_notes() + local gui_notes = arrange_gui_notes() + expect.eq(gui_notes.visible, true) + cleanup(gui_notes) +end + +function test.provide_notes_list() + local notes = { + {name='note 1', comment='comment 1', pos={x=1, y=1, z=1}}, + {name='note 2', comment='comment 2', pos={x=2, y=2, z=2}}, + {name='note 3', comment='comment 3', pos={x=3, y=3, z=3}}, + } + + local gui_notes = arrange_gui_notes({ notes=notes }) + local note_list = gui_notes.subviews.note_list:getChoices() + + for ind, note in ipairs(notes) do + local gui_note = note_list[ind] + expect.eq(gui_note.text, note.name) + expect.eq(gui_note.point.comment, note.comment) + expect.table_eq(gui_note.point.pos, note.pos) + end + + expect.eq(gui_notes.visible, true) + cleanup(gui_notes) +end diff --git a/test/overlay/notes.lua b/test/overlay/notes.lua index 704ceba52e..b096f30399 100644 --- a/test/overlay/notes.lua +++ b/test/overlay/notes.lua @@ -62,7 +62,6 @@ local function add_note(notes_overlay, pos, name, comment) gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'CUSTOM_CTRL_ENTER') end - function test.load_notes_overlay() local notes_overlay = install_notes_overlay() expect.ne(notes_overlay, nil) @@ -167,7 +166,6 @@ end function test.delete_clicked_note() local notes_overlay = install_notes_overlay() - local viewport = guidm.Viewport.get() local half_x = math.floor((viewport.x1 + viewport.x2) / 2) From af1cae28d90956839cc5ff6db5ba4e6aae290f97 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Wed, 19 Feb 2025 22:50:28 +0100 Subject: [PATCH 459/811] Add tests for filtering, centering, preview and delete gui/notes --- gui/notes.lua | 55 ++++++++------- test/gui/notes.lua | 162 ++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 190 insertions(+), 27 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 2e3a80f484..0f4d71f694 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -38,38 +38,43 @@ function NotesWindow:init() self.note_manager = nil self.curr_search_phrase = nil - self:addviews{ + local left_panel_content = { widgets.Panel{ - view_id='note_list_panel', - frame={l=0, w=NOTE_LIST_RESIZE_MIN.w, t=0, b=1}, - visible=true, - frame_inset={l=1,t=1,b=1,r=1}, - autoarrange_subviews=true, + frame={l=0,h=3}, + frame_style=gui.FRAME_INTERIOR, subviews={ - widgets.TextArea{ + widgets.EditField{ view_id='search', - frame={l=0,h=3}, - frame_style=gui.FRAME_INTERIOR, - one_line_mode=true, - on_text_change=self:callback('loadFilteredNotes'), + on_change=self:callback('loadFilteredNotes'), on_submit=function() self.subviews.note_list:submit() end }, - widgets.List{ - view_id='note_list', - frame={l=0,b=2}, - frame_inset={t=1}, - row_height=1, - on_select=function (ind, note) - self:loadNote(note) - end, - on_submit=function (ind, note) - self:loadNote(note) - dfhack.gui.pauseRecenter(note.point.pos) - end - }, - }, + } + }, + widgets.List{ + view_id='note_list', + frame={l=0,b=2}, + frame_inset={t=1}, + row_height=1, + on_select=function (ind, note) + self:loadNote(note) + end, + on_submit=function (ind, note) + self:loadNote(note) + dfhack.gui.pauseRecenter(note.point.pos) + end + }, + } + + self:addviews{ + widgets.Panel{ + view_id='note_list_panel', + frame={l=0, w=NOTE_LIST_RESIZE_MIN.w, t=0, b=1}, + visible=true, + frame_inset={l=1,t=1,b=1,r=1}, + autoarrange_subviews=true, + subviews=left_panel_content, }, widgets.HotkeyLabel{ view_id='create', diff --git a/test/gui/notes.lua b/test/gui/notes.lua index 941faf5e80..9a938b240a 100644 --- a/test/gui/notes.lua +++ b/test/gui/notes.lua @@ -1,6 +1,8 @@ local gui = require('gui') local gui_notes = reqscript('gui/notes') local utils = require('utils') +local guidm = require('gui.dwarfmode') + -- local guidm = require('gui.dwarfmode') @@ -22,7 +24,7 @@ local function arrange_notes(notes) map_points:insert("#", { new=true, - id = waypoints.next_point_id, + id=waypoints.next_point_id, tile=88, fg_color=7, bg_color=0, @@ -30,6 +32,8 @@ local function arrange_notes(notes) comment=note.comment, pos=note.pos }) + + waypoints.next_point_id = waypoints.next_point_id + 1 end end @@ -81,6 +85,160 @@ function test.provide_notes_list() expect.table_eq(gui_note.point.pos, note.pos) end - expect.eq(gui_notes.visible, true) + cleanup(gui_notes) +end + +function test.auto_select_first_note() + local notes = { + {name='green note 1', comment='comment 1', pos={x=1, y=1, z=1}}, + {name='green note 2', comment='comment 2', pos={x=2, y=2, z=2}}, + {name='blue note 3', comment='comment 3', pos={x=3, y=3, z=3}}, + } + + local gui_notes = arrange_gui_notes({ notes=notes }) + expect.eq(gui_notes.subviews.name.text_to_wrap, 'green note 1') + expect.eq(gui_notes.subviews.comment.text_to_wrap, 'comment 1') + + cleanup(gui_notes) +end + +function test.select_on_arrow_up_down() + local notes = { + {name='green note 1', comment='comment 1', pos={x=1, y=1, z=1}}, + {name='green note 2', comment='comment 2', pos={x=2, y=2, z=2}}, + {name='blue note 3', comment='comment 3', pos={x=3, y=3, z=3}}, + } + + local gui_notes = arrange_gui_notes({ notes=notes }) + local screen = dfhack.gui.getCurViewscreen(true) + + gui.simulateInput(screen, 'KEYBOARD_CURSOR_DOWN') + expect.eq(gui_notes.subviews.name.text_to_wrap, 'green note 2') + expect.eq(gui_notes.subviews.comment.text_to_wrap, 'comment 2') + + gui.simulateInput(screen, 'KEYBOARD_CURSOR_DOWN') + expect.eq(gui_notes.subviews.name.text_to_wrap, 'blue note 3') + expect.eq(gui_notes.subviews.comment.text_to_wrap, 'comment 3') + + gui.simulateInput(screen, 'KEYBOARD_CURSOR_DOWN') + expect.eq(gui_notes.subviews.name.text_to_wrap, 'green note 1') + expect.eq(gui_notes.subviews.comment.text_to_wrap, 'comment 1') + + gui.simulateInput(screen, 'KEYBOARD_CURSOR_UP') + expect.eq(gui_notes.subviews.name.text_to_wrap, 'blue note 3') + expect.eq(gui_notes.subviews.comment.text_to_wrap, 'comment 3') + + gui.simulateInput(screen, 'KEYBOARD_CURSOR_UP') + expect.eq(gui_notes.subviews.name.text_to_wrap, 'green note 2') + expect.eq(gui_notes.subviews.comment.text_to_wrap, 'comment 2') + + cleanup(gui_notes) +end + +function test.center_at_submit_note() + local notes = { + {name='green note 1', comment='comment 1', pos={x=1, y=1, z=1}}, + {name='green note 2', comment='comment 2', pos={x=2, y=2, z=2}}, + {name='blue note 3', comment='comment 3', pos={x=3, y=3, z=3}}, + } + + local gui_notes = arrange_gui_notes({ notes=notes }) + local screen = dfhack.gui.getCurViewscreen(true) + + -- it would be best to check viewport, but it's not updated instantly + -- and I do not know way how to force it + -- local viewport = guidm.Viewport.get() + + local last_recenter_pos = nil + mock.patch(dfhack.gui, 'pauseRecenter', function (pos) + last_recenter_pos = pos + end, function () + gui.simulateInput(screen, 'KEYBOARD_CURSOR_DOWN') + gui.simulateInput(screen, 'SELECT') + + expect.eq(last_recenter_pos.x, 2) + expect.eq(last_recenter_pos.y, 2) + expect.eq(last_recenter_pos.z, 2) + + gui.simulateInput(screen, 'KEYBOARD_CURSOR_DOWN') + gui.simulateInput(screen, 'SELECT') + + expect.eq(last_recenter_pos.x, 3) + expect.eq(last_recenter_pos.y, 3) + expect.eq(last_recenter_pos.z, 3) + end) + + cleanup(gui_notes) +end + +function test.filter_notes() + local notes = { + {name='green note 1', comment='comment 1', pos={x=1, y=1, z=1}}, + {name='green note 2', comment='comment 2', pos={x=2, y=2, z=2}}, + {name='blue note 3', comment='comment 3', pos={x=3, y=3, z=3}}, + } + + local gui_notes = arrange_gui_notes({ notes=notes }) + gui_notes.subviews.search:setText('green') + + local note_list = gui_notes.subviews.note_list:getChoices() + expect.eq(#note_list, 2) + + for ind, note in ipairs({table.unpack(notes, 1, 2)}) do + local gui_note = note_list[ind] + expect.eq(gui_note.text, note.name) + expect.eq(gui_note.point.comment, note.comment) + expect.table_eq(gui_note.point.pos, note.pos) + end + + expect.eq(gui_notes.subviews.name.text_to_wrap, 'green note 1') + expect.eq(gui_notes.subviews.comment.text_to_wrap, 'comment 1') + + gui_notes.subviews.search:setText('blue') + + local note_list = gui_notes.subviews.note_list:getChoices() + expect.eq(#note_list, 1) + + expect.eq(note_list[1].text, notes[3].name) + expect.eq(note_list[1].point.comment, notes[3].comment) + expect.table_eq(note_list[1].point.pos, notes[3].pos) + + expect.eq(gui_notes.subviews.name.text_to_wrap, 'blue note 3') + expect.eq(gui_notes.subviews.comment.text_to_wrap, 'comment 3') + + gui_notes.subviews.search:setText('red') + + local note_list = gui_notes.subviews.note_list:getChoices() + expect.eq(#note_list, 0) + + cleanup(gui_notes) +end + +function test.delete_note() + local notes = { + {name='green note 1', comment='comment 1', pos={x=1, y=1, z=1}}, + {name='green note 2', comment='comment 2', pos={x=2, y=2, z=2}}, + {name='blue note 3', comment='comment 3', pos={x=3, y=3, z=3}}, + } + + local gui_notes = arrange_gui_notes({ notes=notes }) + local screen = dfhack.gui.getCurViewscreen(true) + + gui.simulateInput(screen, 'KEYBOARD_CURSOR_DOWN') + gui.simulateInput(screen, 'CUSTOM_CTRL_D') + + local note_list = gui_notes.subviews.note_list:getChoices() + expect.eq(#note_list, 2) + + for ind, note in ipairs({notes[1], notes[3]}) do + local gui_note = note_list[ind] + expect.eq(gui_note.text, note.name) + expect.eq(gui_note.point.comment, note.comment) + expect.table_eq(gui_note.point.pos, note.pos) + end + + expect.eq(gui_notes.subviews.name.text_to_wrap, 'blue note 3') + expect.eq(gui_notes.subviews.comment.text_to_wrap, 'comment 3') + cleanup(gui_notes) end From 2269cbe2b545fc77885709581648d5d815e4f33c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Thu, 20 Feb 2025 07:13:07 +0100 Subject: [PATCH 460/811] Add edit note gui/notes test --- gui/notes.lua | 5 +++++ test/gui/notes.lua | 47 ++++++++++++++++++++++++++++++++++++++++++---- 2 files changed, 48 insertions(+), 4 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 0f4d71f694..149e210260 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -350,6 +350,11 @@ function NotesScreen:onDismiss() if self.should_disable_overlay then overlay.overlay_command({'disable', 'notes.map_notes'}) end + + if self.subviews.notes_window.note_manager then + self.subviews.notes_window.note_manager:dismiss() + end + view = nil end diff --git a/test/gui/notes.lua b/test/gui/notes.lua index 9a938b240a..83013d37ca 100644 --- a/test/gui/notes.lua +++ b/test/gui/notes.lua @@ -2,9 +2,7 @@ local gui = require('gui') local gui_notes = reqscript('gui/notes') local utils = require('utils') local guidm = require('gui.dwarfmode') - - --- local guidm = require('gui.dwarfmode') +local overlay = require('plugins.overlay') config = { target = 'gui/notes', @@ -49,7 +47,11 @@ local function arrange_gui_notes(options) gui_notes:updateLayout() gui_notes:onRender() - return gui_notes + -- for some reasons running tests remove all overlays, + -- but there are need for gui/notes tests + overlay.rescan() + + return gui_notes, gui_notes.subviews.notes_window end local function cleanup(gui_notes) @@ -214,6 +216,43 @@ function test.filter_notes() cleanup(gui_notes) end +function test.edit_note() + local notes = { + {name='green note 1', comment='comment 1', pos={x=1, y=1, z=1}}, + {name='green note 2', comment='comment 2', pos={x=2, y=2, z=2}}, + {name='blue note 3', comment='comment 3', pos={x=3, y=3, z=3}}, + } + + local gui_notes, gui_notes_window = arrange_gui_notes({ notes=notes }) + local screen = dfhack.gui.getCurViewscreen(true) + + gui.simulateInput(screen, 'KEYBOARD_CURSOR_DOWN') + gui.simulateInput(screen, 'CUSTOM_CTRL_E') + + local note_manager = gui_notes_window.note_manager + expect.ne(note_manager, nil) + + expect.eq(note_manager.subviews.name:getText(), 'green note 2') + expect.eq(note_manager.subviews.comment:getText(), 'comment 2') + + note_manager.subviews.name:setText('updated green note 2') + note_manager.subviews.comment:setText('updated comment 2') + local screen = dfhack.gui.getCurViewscreen(true) + printall(screen.widgets) + gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'CUSTOM_CTRL_ENTER') + + local note_list = gui_notes.subviews.note_list:getChoices() + expect.eq(#note_list, 3) + + local updated_note = note_list[2] + + expect.eq(updated_note.text, 'updated green note 2') + expect.eq(updated_note.point.name, 'updated green note 2') + expect.eq(updated_note.point.comment, 'updated comment 2') + + cleanup(gui_notes) +end + function test.delete_note() local notes = { {name='green note 1', comment='comment 1', pos={x=1, y=1, z=1}}, From 038391602b9f8a68a439ab777c8f8d713a41ca08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 23 Feb 2025 08:20:31 +0100 Subject: [PATCH 461/811] Add create new note gui/notes test --- gui/notes.lua | 20 ++++++-------- test/gui/notes.lua | 68 +++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 149e210260..242e9aa8cc 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -9,6 +9,7 @@ local overlay = require 'plugins.overlay' local utils = require 'utils' local note_manager = reqscript('internal/notes/note_manager') +local notes_textures = reqscript('notes').textures local map_points = df.global.plotinfo.waypoints.points @@ -17,13 +18,6 @@ local RESIZE_MIN = {w=65, h=30} local NOTE_SEARCH_BATCH_SIZE = 25 local OVERLAY_NAME = 'notes.map_notes' -local green_pin = dfhack.textures.loadTileset( - 'hack/data/art/note_green_pin_map.png', - 32, - 32, - true -) - NotesWindow = defclass(NotesWindow, widgets.Window) NotesWindow.ATTRS { frame_title='DF Notes', @@ -253,6 +247,7 @@ NotesScreen = defclass(NotesScreen, gui.ZScreen) NotesScreen.ATTRS { focus_path='gui/notes', pass_movement_keys=true, + enable_selector_blink = true, } function NotesScreen:init() @@ -283,7 +278,7 @@ function NotesScreen:onInput(keys) if (keys.SELECT or keys._MOUSE_L) then self.adding_note_pos = dfhack.gui.getMousePos() - local manager = note_manager.NoteManager{ + local note_manager = note_manager.NoteManager{ note=nil, on_update=function() dfhack.run_command_silent('overlay trigger notes.map_notes') @@ -294,7 +289,8 @@ function NotesScreen:onInput(keys) self:stopNoteAdd() end }:show() - manager:setNotePos(self.adding_note_pos) + note_manager:setNotePos(self.adding_note_pos) + self.subviews.notes_window.note_manager = note_manager return true elseif (keys.LEAVESCREEN or keys._MOUSE_R)then @@ -309,7 +305,7 @@ end function NotesScreen:onRenderFrame(dc, rect) NotesScreen.super.onRenderFrame(self, dc, rect) - if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then + if self.enable_selector_blink and not gui.blink_visible(500) then return end @@ -321,7 +317,9 @@ function NotesScreen:onRenderFrame(dc, rect) local function get_overlay_pen(pos) if same_xy(curr_pos, pos) then - local texpos = dfhack.textures.getTexposByHandle(green_pin[1]) + local texpos = dfhack.textures.getTexposByHandle( + notes_textures.green_pin[1] + ) return dfhack.pen.parse{ ch='X', fg=COLOR_BLUE, diff --git a/test/gui/notes.lua b/test/gui/notes.lua index 83013d37ca..270c4fe2cc 100644 --- a/test/gui/notes.lua +++ b/test/gui/notes.lua @@ -3,6 +3,7 @@ local gui_notes = reqscript('gui/notes') local utils = require('utils') local guidm = require('gui.dwarfmode') local overlay = require('plugins.overlay') +local notes_textures = reqscript('notes').textures config = { target = 'gui/notes', @@ -43,6 +44,7 @@ local function arrange_gui_notes(options) gui_notes.main() local gui_notes = gui_notes.view + gui_notes.enable_selector_blink = false gui_notes:updateLayout() gui_notes:onRender() @@ -238,7 +240,6 @@ function test.edit_note() note_manager.subviews.name:setText('updated green note 2') note_manager.subviews.comment:setText('updated comment 2') local screen = dfhack.gui.getCurViewscreen(true) - printall(screen.widgets) gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'CUSTOM_CTRL_ENTER') local note_list = gui_notes.subviews.note_list:getChoices() @@ -281,3 +282,68 @@ function test.delete_note() cleanup(gui_notes) end + +function test.create_new_note() + local notes = { + {name='green note 1', comment='comment 1', pos={x=1, y=1, z=1}}, + } + + local gui_notes, gui_notes_window = arrange_gui_notes({ notes=notes }) + + local note_list = gui_notes.subviews.note_list:getChoices() + expect.eq(#note_list, 1) + + gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'CUSTOM_CTRL_N') + + local viewport = guidm.Viewport.get() + + local half_x = math.floor((viewport.x1 + viewport.x2) / 2) + local half_y = math.floor((viewport.y1 + viewport.y2) / 2) + + local pos = {x=half_x, y=half_y, z=viewport.z} + local screen_pos = viewport:tileToScreen(pos) + df.global.cursor = pos + + -- should not be a test function to map screen tile to mouse pos? + df.global.gps.precise_mouse_x = screen_pos.x * df.global.gps.viewport_zoom_factor / 4 + df.global.gps.precise_mouse_y = screen_pos.y * df.global.gps.viewport_zoom_factor / 4 + + df.global.gps.mouse_x = screen_pos.x + df.global.gps.mouse_y = screen_pos.y + + gui_notes:render(gui.Painter.new()) + + local pen = dfhack.screen.readTile(screen_pos.x, screen_pos.y, true) + + if dfhack.screen.inGraphicsMode() then + local pin_textpos = dfhack.textures.getTexposByHandle( + notes_textures.green_pin[1] + ) + expect.eq(pen and pen.tile, pin_textpos) + else + expect.eq(pen and pen.ch, string.byte('X')) + end + + gui.simulateInput(dfhack.gui.getCurViewscreen(true), '_MOUSE_L') + + local note_manager = gui_notes_window.note_manager + + expect.ne(note_manager, nil) + expect.eq(note_manager.visible, true) + + note_manager.subviews.name:setText('note 2') + note_manager.subviews.comment:setText('new note') + + gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'CUSTOM_CTRL_ENTER') + + local note_list = gui_notes.subviews.note_list:getChoices() + expect.eq(#note_list, 2) + + local gui_note = note_list[2] + expect.eq(gui_note.text, 'note 2') + expect.eq(gui_note.point.comment, 'new note') + expect.table_eq(gui_note.point.pos, pos) + + cleanup(gui_notes) +end + From 3ac3b0e971458a29373e98aa7d4e753a1a35ac3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sun, 23 Feb 2025 09:16:33 +0100 Subject: [PATCH 462/811] Fix notes overlay tests to work in ASCII mode --- gui/notes.lua | 2 +- test/gui/notes.lua | 6 ++-- test/overlay/notes.lua | 66 +++++++++++++++++++++++++----------------- 3 files changed, 44 insertions(+), 30 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 242e9aa8cc..4d8eee7d4d 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -76,7 +76,7 @@ function NotesWindow:init() auto_width=true, label='New note', key='CUSTOM_CTRL_N', - visible=edit_mode, + visible=true, on_activate=function() if self.on_note_add then self:on_note_add() diff --git a/test/gui/notes.lua b/test/gui/notes.lua index 270c4fe2cc..992e812ddd 100644 --- a/test/gui/notes.lua +++ b/test/gui/notes.lua @@ -324,7 +324,10 @@ function test.create_new_note() expect.eq(pen and pen.ch, string.byte('X')) end - gui.simulateInput(dfhack.gui.getCurViewscreen(true), '_MOUSE_L') + gui.simulateInput(dfhack.gui.getCurViewscreen(true), { + _MOUSE_L=true, + _MOUSE_L_DOWN=true, + }) local note_manager = gui_notes_window.note_manager @@ -346,4 +349,3 @@ function test.create_new_note() cleanup(gui_notes) end - diff --git a/test/overlay/notes.lua b/test/overlay/notes.lua index b096f30399..c648cebf29 100644 --- a/test/overlay/notes.lua +++ b/test/overlay/notes.lua @@ -22,7 +22,7 @@ local function install_notes_overlay(options) overlay.rescan() overlay.overlay_command({'enable', 'notes.map_notes'}) - -- if overlay + local overlay_state = overlay.get_state() if not overlay_state.config['notes.map_notes'].enabled then qerror('can not enable notes.map_notes overlay') @@ -62,6 +62,27 @@ local function add_note(notes_overlay, pos, name, comment) gui.simulateInput(dfhack.gui.getCurViewscreen(true), 'CUSTOM_CTRL_ENTER') end +function assert_note_pen(pen) + if dfhack.screen.inGraphicsMode() then + local pin_textpos = dfhack.textures.getTexposByHandle( + notes_textures.green_pin[1] + ) + expect.eq(pen and pen.tile, pin_textpos) + else + expect.eq(pen and pen.ch, string.byte('N')) + end +end + +function set_mouse_screen_pos(screen_pos) + -- should not be a test function to map screen tile to mouse pos? + df.global.gps.precise_mouse_x = screen_pos.x * df.global.gps.viewport_zoom_factor / 4 + df.global.gps.precise_mouse_y = screen_pos.y * df.global.gps.viewport_zoom_factor / 4 + + df.global.gps.mouse_x = screen_pos.x + df.global.gps.mouse_y = screen_pos.y + +end + function test.load_notes_overlay() local notes_overlay = install_notes_overlay() expect.ne(notes_overlay, nil) @@ -90,9 +111,9 @@ function test.render_existing_notes() local half_x = math.floor((viewport.x1 + viewport.x2) / 2) local half_y = math.floor((viewport.y1 + viewport.y2) / 2) - local pos_1 = {x=half_x, y=viewport.y1, z=viewport.z} - local pos_2 = {x=viewport.x1, y=half_y, z=viewport.z} - local pos_3 = {x=half_x, y=half_y, z=viewport.z} + local pos_1 = {x=half_x, y=half_y, z=viewport.z} + local pos_2 = {x=half_x - 2, y=half_y + 2, z=viewport.z} + local pos_3 = {x=half_x + 2, y=half_y + 2, z=viewport.z} add_note(notes_overlay, pos_1, 'note 1', 'first note') add_note(notes_overlay, pos_2, 'note 2', 'second note') @@ -100,16 +121,12 @@ function test.render_existing_notes() reload_notes() - local pin_textpos = dfhack.textures.getTexposByHandle( - notes_textures.green_pin[1] - ) - for _, pos in ipairs({pos_1, pos_2, pos_3}) do notes_overlay:render(gui.Painter.new()) local screen_pos = viewport:tileToScreen(pos) local pen = dfhack.screen.readTile(screen_pos.x, screen_pos.y, true) - expect.eq(pen and pen.tile, pin_textpos) + assert_note_pen(pen) end cleanup(notes_overlay) @@ -123,9 +140,9 @@ function test.edit_clicked_note() local half_x = math.floor((viewport.x1 + viewport.x2) / 2) local half_y = math.floor((viewport.y1 + viewport.y2) / 2) - local pos_1 = {x=half_x, y=viewport.y1, z=viewport.z} - local pos_2 = {x=viewport.x1, y=half_y, z=viewport.z} - local pos_3 = {x=half_x, y=half_y, z=viewport.z} + local pos_1 = {x=half_x, y=half_y, z=viewport.z} + local pos_2 = {x=half_x - 2, y=half_y + 2, z=viewport.z} + local pos_3 = {x=half_x + 2, y=half_y + 2, z=viewport.z} add_note(notes_overlay, pos_1, 'note 1', 'note to edit') add_note(notes_overlay, pos_2, 'note 2', 'other note') @@ -135,19 +152,16 @@ function test.edit_clicked_note() local screen_pos = viewport:tileToScreen(pos_1) - local rect = gui.ViewRect{rect=notes_overlay.frame_rect} + set_mouse_screen_pos(screen_pos) - -- should not be a test function to map screen tile to mouse pos? - df.global.gps.precise_mouse_x = screen_pos.x * df.global.gps.viewport_zoom_factor / 4 - df.global.gps.precise_mouse_y = screen_pos.y * df.global.gps.viewport_zoom_factor / 4 - - local screen = dfhack.gui.getCurViewscreen(true) - gui.simulateInput(screen, { + gui.simulateInput(dfhack.gui.getCurViewscreen(true), { _MOUSE_L=true, + _MOUSE_L_DOWN=true, }) local note_manager = notes_overlay.note_manager expect.ne(note_manager, nil) + expect.eq(note_manager:isDismissed(), false) expect.eq(note_manager.subviews.name:getText(), 'note 1') expect.eq(note_manager.subviews.comment:getText(), 'note to edit') @@ -171,9 +185,9 @@ function test.delete_clicked_note() local half_x = math.floor((viewport.x1 + viewport.x2) / 2) local half_y = math.floor((viewport.y1 + viewport.y2) / 2) - local pos_1 = {x=half_x, y=viewport.y1, z=viewport.z} - local pos_2 = {x=viewport.x1, y=half_y, z=viewport.z} - local pos_3 = {x=half_x, y=half_y, z=viewport.z} + local pos_1 = {x=half_x, y=half_y, z=viewport.z} + local pos_2 = {x=half_x - 2, y=half_y + 2, z=viewport.z} + local pos_3 = {x=half_x + 2, y=half_y + 2, z=viewport.z} add_note(notes_overlay, pos_1, 'note 1', 'note to edit') add_note(notes_overlay, pos_2, 'note 2', 'other note') @@ -185,13 +199,11 @@ function test.delete_clicked_note() local rect = gui.ViewRect{rect=notes_overlay.frame_rect} - -- should not be a test function to map screen tile to mouse pos? - df.global.gps.precise_mouse_x = screen_pos.x * df.global.gps.viewport_zoom_factor / 4 - df.global.gps.precise_mouse_y = screen_pos.y * df.global.gps.viewport_zoom_factor / 4 + set_mouse_screen_pos(screen_pos) - local screen = dfhack.gui.getCurViewscreen(true) - gui.simulateInput(screen, { + gui.simulateInput(dfhack.gui.getCurViewscreen(true), { _MOUSE_L=true, + _MOUSE_L_DOWN=true, }) expect.eq(#map_points, 3) From 104e02bf65d0153f8d7e7a5ff883719309e3d663 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Mon, 24 Feb 2025 07:42:45 +0100 Subject: [PATCH 463/811] Fix notes corner case failed tests Test failed if the cam has been in left-top corner of the map --- test/gui/notes.lua | 18 ++++++++++++++++-- test/overlay/notes.lua | 28 ++++++++++++++++++++-------- 2 files changed, 36 insertions(+), 10 deletions(-) diff --git a/test/gui/notes.lua b/test/gui/notes.lua index 992e812ddd..9bb1c7ac76 100644 --- a/test/gui/notes.lua +++ b/test/gui/notes.lua @@ -66,6 +66,21 @@ local function cleanup(gui_notes) map_points_backup = nil end +function get_visible_map_center() + local viewport = guidm.Viewport.get() + + local half_x = math.max( + math.floor((viewport.x1 + viewport.x2) / 2), + 2 + ) + local half_y = math.max( + math.floor((viewport.y1 + viewport.y2) / 2), + 2 + ) + + return half_x, half_y, viewport.z +end + function test.load_gui_notes() local gui_notes = arrange_gui_notes() expect.eq(gui_notes.visible, true) @@ -297,8 +312,7 @@ function test.create_new_note() local viewport = guidm.Viewport.get() - local half_x = math.floor((viewport.x1 + viewport.x2) / 2) - local half_y = math.floor((viewport.y1 + viewport.y2) / 2) + local half_x, half_y = get_visible_map_center() local pos = {x=half_x, y=half_y, z=viewport.z} local screen_pos = viewport:tileToScreen(pos) diff --git a/test/overlay/notes.lua b/test/overlay/notes.lua index c648cebf29..c6daa5c65d 100644 --- a/test/overlay/notes.lua +++ b/test/overlay/notes.lua @@ -23,11 +23,11 @@ local function install_notes_overlay(options) overlay.rescan() overlay.overlay_command({'enable', 'notes.map_notes'}) - local overlay_state = overlay.get_state() - if not overlay_state.config['notes.map_notes'].enabled then + if not overlay.isOverlayEnabled('notes.map_notes') then qerror('can not enable notes.map_notes overlay') end + local overlay_state = overlay.get_state() return overlay_state.db['notes.map_notes'].widget end @@ -83,6 +83,21 @@ function set_mouse_screen_pos(screen_pos) end +function get_visible_map_center() + local viewport = guidm.Viewport.get() + + local half_x = math.max( + math.floor((viewport.x1 + viewport.x2) / 2), + 2 + ) + local half_y = math.max( + math.floor((viewport.y1 + viewport.y2) / 2), + 2 + ) + + return half_x, half_y, viewport.z +end + function test.load_notes_overlay() local notes_overlay = install_notes_overlay() expect.ne(notes_overlay, nil) @@ -108,8 +123,7 @@ function test.render_existing_notes() local viewport = guidm.Viewport.get() - local half_x = math.floor((viewport.x1 + viewport.x2) / 2) - local half_y = math.floor((viewport.y1 + viewport.y2) / 2) + local half_x, half_y = get_visible_map_center() local pos_1 = {x=half_x, y=half_y, z=viewport.z} local pos_2 = {x=half_x - 2, y=half_y + 2, z=viewport.z} @@ -137,8 +151,7 @@ function test.edit_clicked_note() local viewport = guidm.Viewport.get() - local half_x = math.floor((viewport.x1 + viewport.x2) / 2) - local half_y = math.floor((viewport.y1 + viewport.y2) / 2) + local half_x, half_y, z = get_visible_map_center() local pos_1 = {x=half_x, y=half_y, z=viewport.z} local pos_2 = {x=half_x - 2, y=half_y + 2, z=viewport.z} @@ -182,8 +195,7 @@ function test.delete_clicked_note() local viewport = guidm.Viewport.get() - local half_x = math.floor((viewport.x1 + viewport.x2) / 2) - local half_y = math.floor((viewport.y1 + viewport.y2) / 2) + local half_x, half_y = get_visible_map_center() local pos_1 = {x=half_x, y=half_y, z=viewport.z} local pos_2 = {x=half_x - 2, y=half_y + 2, z=viewport.z} From 622fb2d8d1e149e8ee983eccbeabee97cb2998ff Mon Sep 17 00:00:00 2001 From: Wiktor Obrebski Date: Thu, 27 Feb 2025 19:55:59 +0100 Subject: [PATCH 464/811] Fix notes overlay test --- test/overlay/notes.lua | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/test/overlay/notes.lua b/test/overlay/notes.lua index c6daa5c65d..e1b7a56889 100644 --- a/test/overlay/notes.lua +++ b/test/overlay/notes.lua @@ -28,6 +28,7 @@ local function install_notes_overlay(options) end local overlay_state = overlay.get_state() + return overlay_state.db['notes.map_notes'].widget end @@ -86,16 +87,24 @@ end function get_visible_map_center() local viewport = guidm.Viewport.get() - local half_x = math.max( - math.floor((viewport.x1 + viewport.x2) / 2), - 2 + local map_width, map_height = dfhack.maps.getTileSize() + local world_rect = gui.mkdims_wh(0, 0, map_width, map_height) + -- find center of visible part of the map + local map_rect = gui.ViewRect{rect=world_rect}:viewport(viewport) + + local half_x = math.floor((map_rect.clip_x1 + map_rect.clip_x2) / 2) + local normalized_half_x = math.min( + math.max(half_x, map_rect.clip_x1), + map_rect.clip_x2 ) - local half_y = math.max( - math.floor((viewport.y1 + viewport.y2) / 2), - 2 + + local half_y = math.floor((map_rect.clip_y1 + map_rect.clip_y2) / 2) + local normalized_half_y = math.min( + math.max(half_y, map_rect.clip_y1), + map_rect.clip_y2 ) - return half_x, half_y, viewport.z + return normalized_half_x, normalized_half_y, viewport.z end function test.load_notes_overlay() From da68893d1cf544f2087d550429ad1236aa6e3ed0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wiktor=20Obr=C4=99bski?= Date: Sat, 1 Mar 2025 06:15:56 +0100 Subject: [PATCH 465/811] Fix notes overlay test in case of empty config test suite --- test/overlay/notes.lua | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test/overlay/notes.lua b/test/overlay/notes.lua index e1b7a56889..401e02af94 100644 --- a/test/overlay/notes.lua +++ b/test/overlay/notes.lua @@ -13,6 +13,7 @@ config = { } local map_points_backup = nil +local was_overlay_enabled = overlay.isEnabled() local function install_notes_overlay(options) options = options or {} @@ -20,6 +21,9 @@ local function install_notes_overlay(options) map_points_backup = utils.clone(map_points) map_points:resize(0) + local was_overlay_enabled = overlay.isEnabled() + + overlay.setEnabled(true) overlay.rescan() overlay.overlay_command({'enable', 'notes.map_notes'}) @@ -48,6 +52,8 @@ local function cleanup(notes_overlay) map_points_backup = nil reload_notes() + + overlay.setEnabled(was_overlay_enabled) end local function add_note(notes_overlay, pos, name, comment) From 33f0a9d65e0ed76ad7823f74180454729357785d Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 2 Mar 2025 12:11:03 +0100 Subject: [PATCH 466/811] spectate.lua: add "hold-to-show" option --- gui/spectate.lua | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/gui/spectate.lua b/gui/spectate.lua index 416c48e94b..3fb6ec49eb 100644 --- a/gui/spectate.lua +++ b/gui/spectate.lua @@ -46,7 +46,7 @@ end Spectate = defclass(Spectate, widgets.Window) Spectate.ATTRS { frame_title='Spectate', - frame={l=5, t=5, w=36, h=39}, + frame={l=5, t=5, w=36, h=40}, } local function create_toggle_button(frame, cfg_elem, hotkey, label, cfg_elem_key) @@ -225,11 +225,26 @@ function Spectate:init() text='Hover', }, create_row({t=21}, 'Enabled', 'E', '', colFollow, colHover), + create_numeric_edit_field({t=23}, 'tooltip-follow-blink-milliseconds', 'CUSTOM_B', 'Blink period (ms): '), - create_row({t=25}, 'Job', 'J', 'job', colFollow, colHover), - create_row({t=26}, 'Name', 'N', 'name', colFollow, colHover), - create_row({t=27}, 'Stress', 'S', 'stress', colFollow, colHover), - create_stress_list({t=28}, colFollow, colHover), + widgets.CycleHotkeyLabel{ + frame={t=24}, + key='CUSTOM_C', + label="Hold to show:", + options={ + {label="None", value="none", pen=COLOR_GREY}, + {label="Ctrl", value="ctrl", pen=COLOR_LIGHTCYAN}, + {label="Alt", value="alt", pen=COLOR_LIGHTCYAN}, + {label="Shift", value="shift", pen=COLOR_LIGHTCYAN}, + }, + initial_option=spectate.get_config_elem('tooltip-follow-hold-to-show'), + on_change=function(new, _) dfhack.run_command('spectate', 'set', 'tooltip-follow-hold-to-show', new) end + }, + + create_row({t=26}, 'Job', 'J', 'job', colFollow, colHover), + create_row({t=27}, 'Name', 'N', 'name', colFollow, colHover), + create_row({t=28}, 'Stress', 'S', 'stress', colFollow, colHover), + create_stress_list({t=29}, colFollow, colHover), } end From 8305ac09c27be2e3da8a02198caa3ac1c9f7f6ef Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 3 Mar 2025 19:32:54 +0000 Subject: [PATCH 467/811] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.31.1 → 0.31.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.1...0.31.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 66e3d0f1cb..14a734aa5a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.1 + rev: 0.31.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 271896f4eae9ad44cab1238afd92c041fc3895c8 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Wed, 5 Mar 2025 19:22:36 +0100 Subject: [PATCH 468/811] do not assign crafting jobs to nobles holding meetings --- changelog.txt | 1 + idle-crafting.lua | 2 ++ 2 files changed, 3 insertions(+) diff --git a/changelog.txt b/changelog.txt index 7d89fcd860..7b530fff3a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -38,6 +38,7 @@ Template for new versions: ## Fixes - `position`: support for adv mode look cursor - `gui/liquids`: using the remove tool with magma selected will no longer create unexpected unpathable tiles +- `idle-crafting`: do not assign crafting jobs to nobles holding meetings (avoid dangling jobs) ## Misc Improvements - `hide-tutorials`: handle tutorial popups for adventure mode diff --git a/idle-crafting.lua b/idle-crafting.lua index 39a647731c..d67f8122a2 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -333,6 +333,8 @@ end function unitIsAvailable(unit) if unit.job.current_job then return false + elseif #unit.specific_refs > 0 then -- activities such as "Conduct Meeting" + return false elseif #unit.social_activities > 0 then return false elseif #unit.individual_drills > 0 then From de91116d8698f3568f60e10531c1423658d26041 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Thu, 6 Mar 2025 16:22:15 -0800 Subject: [PATCH 469/811] gui/adv-finder * Create adv-finder.lua * Create adv-finder.rst * Update changelog.txt * Update aquifer.rst - fix keybind label * Update stuckdoors.lua - isActive by default --- changelog.txt | 1 + docs/gui/adv-finder.rst | 112 ++++++ docs/gui/aquifer.rst | 2 +- fix/stuckdoors.lua | 2 +- gui/adv-finder.lua | 773 ++++++++++++++++++++++++++++++++++++++++ 5 files changed, 888 insertions(+), 2 deletions(-) create mode 100644 docs/gui/adv-finder.rst create mode 100644 gui/adv-finder.lua diff --git a/changelog.txt b/changelog.txt index 7b530fff3a..c28ff421b6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk - `gui/spectate`: interactive UI for configuring new `spectate` features - `gui/notes`: UI for adding and managing notes attached to tiles on the map +- `gui/adv-finder`: UI for tracking historical figures and artifacts in adventure mode ## New Features - `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys diff --git a/docs/gui/adv-finder.rst b/docs/gui/adv-finder.rst new file mode 100644 index 0000000000..a1d12128e7 --- /dev/null +++ b/docs/gui/adv-finder.rst @@ -0,0 +1,112 @@ +gui/adv-finder +============== + +.. dfhack-tool:: + :summary: Find and track historical figures and artifacts + :tags: adventure armok inspection items units + +A real-time tracker for historical figures and artifacts. Select a target by +clicking the settings icon [☼] and selecting an entry from the list in the +relevant tab. The list can be filtered by search string, as well as by +excluding dead figures (displayed in red text). Artifacts can exclude books, +and the "dead" option excludes artifacts held by dead figures (which are +generally unrecoverable). Dismissing the screen (e.g., right-click) will +close the target search window first. A second dismissal will close the +finder window, but target settings will be preserved until the world is +unloaded. + +Your coordinates will be kept up to date alongside your target's. There are +two types of coordinates, and they will be displayed as long as they can be +determined. + +========== ========== +Coord Type Meaning +========== ========== +Global Distance in map blocks from the world origin (northwest corner). + The adventurer usually moves by 3 blocks during fast travel, but + slows to 1 when the zoomed site map is displayed. Equivalent to + 16 local tiles. Always available except for targets with an + indeterminate location. +Local Tile coordinates, available outside of fast travel and sleeping. + Your target's local coordinates are displayed when nearby and + loaded. Local coordinates will remain consistent within a site, but + may jump around in the wilderness as areas of the world are loaded. +========== ========== + +For global coordinates, the Z component will only be displayed if it can be +specifically determined by the location type. This represents an underground +layer depth, so the surface is indicated by ``Z0`` and the first cavern layer +is ``Z-1``. + +A compass and relative coordinates will be displayed. The relative coordinate +display uses the most precise coordinate type shared between you and your +target. + +There are six types of location types displayed for targets: + +============= ========== +Location Type Meaning +============= ========== +Nearby The target is loaded into the map area and the local + coordinates will be displayed. If you don't see this when you're + in the correct area and outside fast travel, then the target + isn't loading for some reason and you'll never be able to find + them. +Site The target is located within a site. The text displays + "At " and the global coords will represent the center + of the site if the target doesn't track its own precise + coordinates (e.g., worldgen being vague). +Traveling The target is traveling around the world map like an army. +Wilderness The target is somewhere on the surface not in a site. +Underground The target is somewhere in the caverns not in a site. +None The target's location isn't defined in the game world. + Maybe they're a deity. Maybe they got dropped off in limbo + after their army disbanded. If they're dead, the location + wasn't recorded properly in history. The text displays "Missing" + if they're dead or can die of old age, else "Transcendent" + because nothing can touch them. +============= ========== + +Dead figures generally can't be encountered at all, and they take their items +with them if they weren't separated properly by worldgen. The coord given is +usually a death or abstract burial location, but the corpse isn't guaranteed to +exist. Generally, wilderness and underground locations only have coords if you +left something there in adventure mode. Anything lost there during worldgen or a +fort mode mission likely can't be located. Anything in a site is usually a safe +bet, but sometimes items won't load. (Fort missions can be used to acquire these +for later retrieval, however.) Traveling targets are always valid. + +Usage +----- + +:: + + gui/adv-finder [] + +Examples +-------- + +``gui/adv-finder`` + Open the finder window (unless already open). Target will be blank on first + use, but maintained on future invocations. +``gui/adv-finder --histfig 1234`` + Track the historical figure with ID #1234. Finder will be opened if not + already. +``gui/adv-finder -h -1 -a -1`` + Clear any target so it's just the adventurer. Finder will be opened if not + already. +``gui/adv-finder --debug`` + Display selected target IDs in the finder window title bar. Finder will be + opened if not already. This setting isn't saved, so it can be disabled by + invoking ``gui/adv-finder`` again without the option. + +Options +------- + +``-h``, ``--histfig `` + Set the target to the historical figure with the given ID. +``-a``, ``--artifact `` + Set the target to the artifact record with the given ID. (Not an item ID!) +``-d``, ``--debug`` + Display selected target IDs in the finder window title bar. Doesn't persist + between invocations. diff --git a/docs/gui/aquifer.rst b/docs/gui/aquifer.rst index 52d47541b8..a595e197d1 100644 --- a/docs/gui/aquifer.rst +++ b/docs/gui/aquifer.rst @@ -12,7 +12,7 @@ tiles or warm tiles). Note that "just damp" tiles will still be highlighted if they are otherwise already visible. You can draw boxes around areas of tiles to alter their aquifer properties, or -you can use the :kbd:`Ctrl`:kbd:`A`` shortcut to affect entire layers at a time. +you can use the :kbd:`Ctrl`:kbd:`A` shortcut to affect entire layers at a time. If you want to see where the aquifer tiles are so you can designate digging, please run `gui/reveal`. If you only want to see the aquifer tiles and not diff --git a/fix/stuckdoors.lua b/fix/stuckdoors.lua index 8e1e1bc026..ec48a847b5 100644 --- a/fix/stuckdoors.lua +++ b/fix/stuckdoors.lua @@ -9,7 +9,7 @@ end -- Util function: find out if there are any units on the tile with coordinates x,y,z function unitOnTile(x, y, z) - local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z,dfhack.units.isActive) + local units = dfhack.units.getUnitsInBox(x,y,z,x,y,z) return #(units) > 0 end diff --git a/gui/adv-finder.lua b/gui/adv-finder.lua new file mode 100644 index 0000000000..9831b0b04e --- /dev/null +++ b/gui/adv-finder.lua @@ -0,0 +1,773 @@ +-- Find and track historical figures and artifacts +--@module = true + +local argparse = require('argparse') +local gui = require('gui') +local widgets = require('gui.widgets') +local utils = require('utils') + +local world = df.global.world +local transName = dfhack.translation.translateName +local findHF = df.historical_figure.find +local toSearch = dfhack.toSearchNormalized + +LType = utils.invert{'None','Local','Site','Wild','Under','Army'} --Location type + +filter_text = filter_text --Stored filter between lists; for setting only! +-- Use AdvSelWindow:get_filter_text() instead for getting current filter +cur_tab = cur_tab or 1 -- 1: HF, 2: Artifact +show_dead = show_dead or false --Exclude dead HFs +show_books = show_books or false --Exclude books +sel_hf = sel_hf or -1 --Selected historical_figure.id +sel_art = sel_art or -1 --Selected artifact_record.id +debug_id = false --Show target ID in window title; reopening without -d option resets + +---- Fns for target names ---- + +local function get_race_name(hf) --E.g., 'Plump Helmet Man' + return dfhack.capitalizeStringWords(dfhack.units.getRaceReadableNameById(hf.race)) +end + +function get_hf_name(hf) --'Native Name "Translated Name", Race' + local full_name = transName(hf.name, false) + if full_name == '' then --Improve searchability + full_name = 'Anonymous' + else --Add the translation + local t_name = transName(hf.name, true) + if full_name ~= t_name then --Don't repeat + full_name = full_name..' "'..t_name..'"' + end + end + local race_name = get_race_name(hf) + if race_name == '' then --Elf deities don't have a race + full_name = full_name..', Force' + else --Add the race + full_name = full_name..', '..race_name + end + return full_name +end + +function get_art_name(ar) --'Native Name "Translated Name", Item' + local full_name = transName(ar.name, false) + if full_name == '' then --Improve searchability + full_name = 'Anonymous' + else --Add the translation + local t_name = transName(ar.name, true) + if full_name ~= t_name then --Don't repeat + full_name = full_name..' "'..t_name..'"' + end + end + return full_name..', '..dfhack.items.getDescription(ar.item, 1, true) +end + +local function build_hf_list() --Build alphabetized HF list + local t = {} + for _,hf in ipairs(world.history.figures) do + if show_dead or hf.died_year == -1 then --Filter dead + local name = get_hf_name(hf) + local str = toSearch(name) + + if hf.died_year ~= -1 then + name = {{text=name, pen=COLOR_RED}} --Dead + elseif not hf.info or not hf.info.whereabouts then + name = {{text=name, pen=COLOR_YELLOW}} --Deity (usually) + end + table.insert(t, {text=name, id=hf.id, search_key=str}) + end + end + table.sort(t, function(a, b) return a.search_key < b.search_key end) + return t +end + +local function get_id(first, second) --Try to get a numeric id or -1 + return (first >= 0 and first) or (second >= 0 and second) or -1 +end + +local function dead_holder(ar) --Return true if has holder and they're dead + local holder = df.historical_figure.find(get_id(ar.holder_hf, ar.owner_hf)) + return holder and holder.died_year ~= -1 +end + +local function is_book(ar) --Return true if codex/scroll/quire + local item = ar.item + return item._type == df.item_bookst or --We'll ignore slabs, despite legends mode behaviour + (item._type == df.item_toolst and item:hasToolUse(df.tool_uses.CONTAIN_WRITING)) +end + +local function build_art_list() --Build alphabetized artifact list + local t = {} + for _,ar in ipairs(world.artifacts.all) do + local dead = dead_holder(ar) + if (show_dead or not dead) and (show_books or not is_book(ar)) then + local name = get_art_name(ar) + local str = toSearch(name) + + if dead then + name = {{text=name, pen=COLOR_RED}} + end + table.insert(t, {text=name, id=ar.id, search_key=str}) + end + end + table.sort(t, function(a, b) return a.search_key < b.search_key end) + return t +end + +------------------ +-- AdvSelWindow -- +------------------ + +AdvSelWindow = defclass(AdvSelWindow, widgets.Window) +AdvSelWindow.ATTRS{ + frame_title = 'Find Target', + frame = {w=42, h=24, t=22, r=34}, + resizable = true, + visible = false, +} + +function AdvSelWindow:init() + self:addviews{ + widgets.TabBar{ + frame = {t=0}, + labels = { + 'Historical Figures', + 'Artifacts', + }, + on_select = self:callback('swap_tab'), + get_cur_page = function() return cur_tab end, + }, + widgets.FilteredList{ + view_id = 'sel_hf_list', + frame = {t=2, b=2}, + not_found_label = 'No results', + edit_key = 'CUSTOM_ALT_S', + on_submit = self:callback('select_entry'), + visible = false, --Handled in sel_list + }, + widgets.FilteredList{ --setChoices is too slow, don't reuse HF list + view_id = 'sel_art_list', + frame = {t=2, b=2}, + not_found_label = 'No results', + edit_key = 'CUSTOM_ALT_S', + on_submit = self:callback('select_entry'), + visible = false, + }, + widgets.ToggleHotkeyLabel + { + view_id = 'dead_toggle', + frame = {b=0, l=0, w=17, h=1}, + label = 'Show dead:', + key = 'CUSTOM_SHIFT_D', + initial_option = show_dead, + on_change = self:callback('set_show_dead'), + }, + widgets.ToggleHotkeyLabel + { + view_id = 'book_toggle', + frame = {b=0, r=0, w=18, h=1}, + label = 'Show books:', + key = 'CUSTOM_SHIFT_B', + initial_option = show_books, + on_change = self:callback('set_show_books'), + visible = function() return cur_tab ~= 1 end, + }, + } +end + +function AdvSelWindow:get_filter_text() --Get current filter from tab + if cur_tab == 1 then --HF + return self.subviews.sel_hf_list:getFilter() + else --Artifact + return self.subviews.sel_art_list:getFilter() + end +end + +function AdvSelWindow:swap_tab(idx) --Persist filter and swap list + if cur_tab ~= idx then + filter_text = self:get_filter_text() + cur_tab = idx + self:sel_list() + end +end + +function AdvSelWindow:sel_list() --Set correct list for tab + local new, old, build_fn + if cur_tab == 1 then --HF + new = self.subviews.sel_hf_list + old = self.subviews.sel_art_list + build_fn = build_hf_list + else --Artifact + new = self.subviews.sel_art_list + old = self.subviews.sel_hf_list + build_fn = build_art_list + end + + old.visible = false + new.visible = true + if not next(new:getChoices()) then --Empty, build list + new:setChoices(build_fn()) + end + new:setFilter(filter_text) --Restore filter + new.edit:setFocus(old.edit.focus) --Inherit search focus + old.edit:setFocus(false) +end + +function AdvSelWindow:select_entry(sel, obj) --Set correct target for tab + local id = obj and obj.id or -1 + if cur_tab == 1 then --HF + sel_hf, sel_art = id, -1 + else --Artifact + sel_hf, sel_art = -1, id + end +end + +function AdvSelWindow:set_show_dead(show) --Set filtering of dead HFs, rebuild list + show = not not show --To bool + if show == show_dead then + return --No change + end + show_dead = show + filter_text = self:get_filter_text() + self.subviews.sel_hf_list:setChoices() + self.subviews.sel_art_list:setChoices() --Held by HF + self:sel_list() +end + +function AdvSelWindow:set_show_books(show) --Set filtering of books, rebuild list + show = not not show + if show == show_books then + return + end + show_books = show + filter_text = self:get_filter_text() + self.subviews.sel_art_list:setChoices() + self:sel_list() +end + +function AdvSelWindow:onInput(keys) --Close only this window + if keys.LEAVESCREEN or keys._MOUSE_R then + self.visible = false + filter_text = self:get_filter_text() + self.subviews.sel_hf_list:setChoices() + self.subviews.sel_art_list:setChoices() + return true + end + return self.super.onInput(self, keys) +end + +---- Fns for getting adventurer data ---- + +function global_from_local(pos) --Calc global coords (blocks from world origin) from local map pos + return pos and {x = world.map.region_x*3 + pos.x//16, y = world.map.region_y*3 + pos.y//16} or nil +end + +function get_adv_data() --All the coords we can get + local adv = dfhack.world.getAdventurer() + if not adv then --Army exists when unit doesn't + local army = df.army.find(df.global.adventure.player_army_id) + if army then --Should always exist if unit doesn't + return {g_pos = army.pos} + end + return nil --Error + end + return {g_pos = global_from_local(adv.pos), pos = adv.pos} +end + +---- Fns for getting target data ---- + +local function div(n, d) return n//d, n%d end +--We can get the MLT coords of a CZ from its ID (e.g., hf.info.whereabouts.cz_id) +--The g_pos will represent the center of the 3x3 MLT +--In testing, the HF of interest remained in limbo, but it might be of use to someone +function cz_g_pos(cz_id) --Creation zone center in global coords + if not cz_id or cz_id < 0 then return nil end + local w, t, rem = world.world_data.world_width, {}, nil + t.reg_y, rem = div(cz_id, 16*16*w) + t.mlt_y, rem = div(rem, 16*w) + t.reg_x, t.mlt_x = div(rem, 16) + return {x = (t.reg_x*16 + t.mlt_x)*3+1, y = (t.reg_y*16 + t.mlt_y)*3+1} +end + +function site_g_pos(site) --Site center in global coords (blocks from world origin) + local x, y = site.global_min_x*3, site.global_min_y*3 + x, y = x + (site.global_max_x*3 - x)//2, y + (site.global_max_y*3 - y)//2 + return {x = x, y = y} +end + +local function apply_site_z(site, g_pos) --Improve Z coord using site + local pos = g_pos or site_g_pos(site) --Fall back on site center + pos.z = site.min_depth == site.max_depth and site.min_depth or nil --Single layer site + return pos --Return new table +end + +local function death_at_idx(idx) --Return death location data + if idx then --Dead + local event = world.history.events_death[idx] + return {site = event.site, sr = event.subregion, layer = event.feature_layer} + end + return {site = -1, sr = -1, layer = -1} --Alive +end + +local death_hfid, death_found_idx, death_last_idx --Cache history.events_death data +function get_death_data(hf) --Try to get death location data + if hf.died_year == -1 then --Alive (or undead) + return death_at_idx() + elseif hf.id ~= death_hfid then --Wrong HF, clear cache + death_hfid, death_found_idx, death_last_idx = hf.id, nil, nil + end + local deaths = world.history.events_death + local deaths_end = #deaths-1 + + if death_last_idx and death_last_idx == deaths_end then --No new entries + return death_at_idx(death_found_idx) --Use cached death + end + death_last_idx = death_last_idx or 0 --First time search entire vector + + for i=deaths_end, death_last_idx, -1 do --Iterate new entries backwards + local event = deaths[i] + if event._type == df.history_event_hist_figure_diedst then + if event.victim_hf == hf.id then + death_found_idx = i --Cache HF's most recent death + break + end + elseif event._type == df.history_event_hist_figure_revivest then + if event.histfig == hf.id then --Just in case died_year check failed somehow + death_found_idx = nil --Clear death state + break + end + end + end + death_last_idx = deaths_end --Cache latest index + return death_at_idx(death_found_idx) +end + +local function get_whereabouts(hf) --Return state profile data + local w = hf and hf.info and hf.info.whereabouts + if w then + local g_pos = w.abs_smm_x >= 0 and {x = w.abs_smm_x, y = w.abs_smm_y} or nil + return {site = w.site_id, sr = w.subregion_id, layer = w.feature_layer_id, army = w.army_id, g_pos = g_pos} + end + return {site = -1, sr = -1, layer = -1, army = -1} +end + +function get_hf_data(hf) --Locational data and coords + if not hf then --No target + return nil + end + + local where = get_whereabouts(hf) + for _,unit in ipairs(world.units.active) do + if unit.id == hf.unit_id then --Unit is loaded and active (i.e., player not traveling) + local pos = xyz2pos(dfhack.units.getPosition(unit)) + pos = pos.x >= 0 and pos or nil --Avoid bad coords + local g_pos = global_from_local(pos) or where.g_pos + return {loc_type = LType.Local, g_pos = g_pos, pos = pos} + end + end + local death = get_death_data(hf) + + local site = df.world_site.find(get_id(where.site, death.site)) + if site then --Site + return {loc_type = LType.Site, site = site, g_pos = apply_site_z(site, where.g_pos)} + end + + local sr = df.world_region.find(get_id(where.sr, death.sr)) + if sr then --Surface biome + if where.g_pos then + where.g_pos.z = 0 --Must be surface + end + return {loc_type = LType.Wild, sr = sr, g_pos = where.g_pos} + end + + local layer = df.world_underground_region.find(get_id(where.layer, death.layer)) + if layer then --Cavern layer + if where.g_pos then + where.g_pos.z = layer.layer_depth + end + return {loc_type = LType.Under, g_pos = where.g_pos} + end + + local army = df.army.find(where.army) + if army then --Traveling + return {loc_type = LType.Army, g_pos = army.pos} + end + + if #hf.site_links > 0 then --Try to grab site from links + local site = df.world_site.find(hf.site_links[#hf.site_links-1].site) --Only try last link + if site and utils.binsearch(site.populace.nemesis, hf.nemesis_id) then --HF is present + return {loc_type = LType.Site, site = site, g_pos = apply_site_z(site, where.g_pos)} + end + end + --We'd try cz_g_pos here if it actually helped + return {loc_type = LType.None, g_pos = where.g_pos} --Probably in limbo +end + +function get_art_data(ar) --Locational data and coords + if not ar then --No target + return nil + end + local holder = findHF(get_id(ar.holder_hf, ar.owner_hf)) + local data = get_hf_data(holder) or {loc_type = LType.None} + data.holder = holder + + local g_pos = ar.abs_tile_x >= 0 and {x = ar.abs_tile_x//16, y = ar.abs_tile_y//16} or nil + + for _,item in ipairs(world.items.other.ANY_ARTIFACT) do + if item == ar.item then --Item is nearby if categorized + local pos = xyz2pos(dfhack.items.getPosition(item)) + pos = pos.x >= 0 and pos or nil --Avoid bad coords + g_pos = global_from_local(pos) or g_pos + return {loc_type = LType.Local, holder = holder, g_pos = g_pos, pos = pos} + end + end + + local site = df.world_site.find(get_id(ar.site, ar.storage_site)) + if site then --Site + return {loc_type = LType.Site, site = site, holder = holder, g_pos = apply_site_z(site, g_pos)} + end + + if data.loc_type ~= LType.None then --Inherit from holder (seems lower priority than site) + return data + end + + local sr = df.world_region.find(get_id(ar.subregion, ar.loss_region)) + if sr then --Surface biome + if g_pos then + g_pos.z = 0 --Must be surface + end + return {loc_type = LType.Wild, holder = holder, sr = sr, g_pos = g_pos} + end + + local layer = df.world_underground_region.find(get_id(ar.feature_layer, ar.last_layer)) + if layer then --Cavern layer + if g_pos then + g_pos.z = layer.layer_depth + end + return {loc_type = LType.Under, holder = holder, g_pos = g_pos} + end + + data.g_pos = data.g_pos or g_pos or nil --Try our own if no holder g_pos + return data --Probably in limbo +end + +---- Fns for adventurer info panel ---- + +local compass_dir = { + 'E','ENE','NE','NNE', + 'N','NNW','NW','WNW', + 'W','WSW','SW','SSW', + 'S','SSE','SE','ESE', +} +local compass_pointer = { --Same chars as movement indicators + '>',string.char(191),string.char(191),string.char(191), + '^',string.char(218),string.char(218),string.char(218), + '<',string.char(192),string.char(192),string.char(192), + 'v',string.char(217),string.char(217),string.char(217), +} + +local idx_div_two_pi = 16/(2*math.pi) --16 indices / 2*Pi radians +function compass(dx, dy) --Handy compass strings + if dx*dx + dy*dy == 0 then --On target + return '***', string.char(249) --Char 249 is centered dot + end + local angle = math.atan(-dy, dx) --North is -Y + local index = math.floor(angle*idx_div_two_pi + 16.5)%16 --0.5 helps rounding + return compass_dir[index + 1], compass_pointer[index + 1] +end + +local function insert_text(t, text) --Insert newline before text + if text and text ~= '' then + table.insert(t, NEWLINE) + table.insert(t, text) + end +end + +local function relative_text(t, adv_data, target_data) --Add relative coords and compass + if not target_data then --No target + return + end + if target_data.pos and adv_data.pos then --Use local + local dx = target_data.pos.x - adv_data.pos.x + local dy = target_data.pos.y - adv_data.pos.y + local dir, point = compass(dx, dy) + table.insert(t, NEWLINE) --Improve visibility + insert_text(t, 'Target (local):') + insert_text(t, point..' '..dir) + insert_text(t, ('X%+d Y%+d Z%+d'):format(dx, dy, target_data.pos.z - adv_data.pos.z)) + elseif target_data.g_pos and adv_data.g_pos then --Use global + local dx = target_data.g_pos.x - adv_data.g_pos.x + local dy = target_data.g_pos.y - adv_data.g_pos.y + local dir, point = compass(dx, dy) + table.insert(t, NEWLINE) + insert_text(t, {text='Target (global):', pen=COLOR_GREY}) + insert_text(t, {text=point..' '..dir, pen=COLOR_GREY}) + + local str = ('X%+d Y%+d'):format(dx, dy) + if target_data.g_pos.z and adv_data.g_pos.z then --Use Z if we have it + str = str..(' Z%+d'):format(adv_data.g_pos.z - target_data.g_pos.z) --Negate because it's depth + end + insert_text(t, {text=str, pen=COLOR_GREY}) + end --else insufficient data +end + +local function pos_text(t, g_pos, pos) --Add available coords + if g_pos then + local str = g_pos.z and (' Z'..-g_pos.z) or '' --Use Z if we have it, negate because it's depth + insert_text(t, {text='Global: X'..g_pos.x..' Y'..g_pos.y..str, pen=COLOR_GREY}) + else --Keep compass in consistent spot + table.insert(t, NEWLINE) + end + if pos then + insert_text(t, ('Local: X%d Y%d Z%d'):format(pos.x, pos.y, pos.z)) + else + table.insert(t, NEWLINE) + end +end + +local function adv_text(adv_data, target_data) --Text for adv info panel + if not adv_data then + return 'Error' + end + local t = {'You'} --You, global, local, relative + pos_text(t, adv_data.g_pos, adv_data.pos) + + relative_text(t, adv_data, target_data) + return t +end + +---- Fns for target info panel ---- + +local function insert_name_text(t, name) --HF or artifact name; Return true if both lines + local str = transName(name, false) + if str == '' then + table.insert(t, 'Anonymous') + else --Both native and translation + table.insert(t, str) --Native + local t_name = transName(name, true) + if str ~= t_name then --Don't repeat + insert_text(t, '"'..t_name..'"') + return true + end + end +end + +local function hf_text(hf, target_data) --HF text for target info panel + if not hf or not target_data then --No target + return '' + end + local t = {} --Native, [translated], race, alive, location, global, local + + local both_lines = insert_name_text(t, hf.name) + local str = get_race_name(hf) + insert_text(t, str ~= '' and str or 'Force') + if not both_lines then --Consistent spacing + table.insert(t, NEWLINE) + end + + local eternal --Can't reasonably die + if hf.died_year ~= -1 then + insert_text(t, {text='DEAD', pen=COLOR_RED}) + elseif hf.old_year == -1 and target_data.loc_type == LType.None then + eternal = true --In limbo and can't reasonably die + insert_text(t, {text='ETERNAL', pen=COLOR_LIGHTBLUE}) + else + insert_text(t, {text='ALIVE', pen=COLOR_LIGHTGREEN}) + end + + if target_data.loc_type == LType.None then --Everywhere or nowhere + if eternal then + insert_text(t, {text='Transcendent', pen=COLOR_YELLOW}) + else + insert_text(t, {text='Missing', pen=COLOR_MAGENTA}) + end + else --Physical location + if target_data.loc_type == LType.Local then + insert_text(t, 'Nearby') + elseif target_data.loc_type == LType.Site then + insert_text(t, {text='At '..transName(target_data.site.name, true), pen=COLOR_LIGHTBLUE}) + elseif target_data.loc_type == LType.Army then + insert_text(t, {text='Traveling', pen=COLOR_LIGHTBLUE}) + elseif target_data.loc_type == LType.Wild then + insert_text(t, {text='Wilderness ('..transName(target_data.sr.name, true)..')', pen=COLOR_LIGHTRED}) + elseif target_data.loc_type == LType.Under then + insert_text(t, {text='Underground', pen=COLOR_LIGHTRED}) + else --Undefined loc_type + insert_text(t, {text='Error', pen=COLOR_MAGENTA}) + end + end + pos_text(t, target_data.g_pos, target_data.pos) + return t +end + +local function art_text(art, target_data) --Artifact text for target info panel + if not art or not target_data then --No target + return '' + end + local t = {} --Native, [translated], item_type, [held,] location, global, local + + local both_lines = insert_name_text(t, art.name) + insert_text(t, dfhack.items.getDescription(art.item, 1, true)) + if not both_lines then --Consistent spacing + table.insert(t, NEWLINE) + end + + if target_data.holder then + local str = 'Held by '..transName(target_data.holder.name, false) + insert_text(t, {text=str, pen=(target_data.holder.died_year == -1 and COLOR_LIGHTGREEN or COLOR_RED)}) + else --Consistent spacing + table.insert(t, NEWLINE) + end + + if target_data.loc_type == LType.None then + insert_text(t, {text='Missing', pen=COLOR_MAGENTA}) + elseif target_data.loc_type == LType.Local then + insert_text(t, 'Nearby') + elseif target_data.loc_type == LType.Site then + insert_text(t, {text='At '..transName(target_data.site.name, true), pen=COLOR_LIGHTBLUE}) + elseif target_data.loc_type == LType.Army then + insert_text(t, {text='Traveling', pen=COLOR_LIGHTBLUE}) + elseif target_data.loc_type == LType.Wild then + insert_text(t, {text='Wilderness ('..transName(target_data.sr.name, true)..')', pen=COLOR_LIGHTRED}) + elseif target_data.loc_type == LType.Under then + insert_text(t, {text='Underground', pen=COLOR_LIGHTRED}) + else --Undefined loc_type + insert_text(t, {text='Error', pen=COLOR_MAGENTA}) + end + pos_text(t, target_data.g_pos, target_data.pos) + return t +end + +------------------- +-- AdvFindWindow -- +------------------- + +AdvFindWindow = defclass(AdvFindWindow, widgets.Window) +AdvFindWindow.ATTRS{ + frame_title = 'Finder', + frame = {w=30, h=24, t=22, r=2}, + resizable = true, +} + +function AdvFindWindow:init() + self:addviews{ + widgets.Panel{ + view_id = 'adv_panel', + frame = {t=1, h=9}, + frame_style = gui.FRAME_INTERIOR, + subviews = { + widgets.Label{ + view_id = 'adv_label', + text = '', + frame = {t=0}, + }, + }, + }, + widgets.Panel{ + view_id = 'target_panel', + frame = {t=11}, + frame_style = gui.FRAME_INTERIOR, + subviews = { + widgets.Label{ + view_id = 'target_label', + text = '', + frame = {t=0}, + }, + }, + }, + widgets.ConfigureButton{ + frame = {t=0, r=0}, + on_click = function() + local sel_window = view.subviews[2] --AdvSelWindow + sel_window.visible = true + sel_window:sel_list() + end, + } + } +end + +local function set_title(self) --Display target ID in title + if debug_id then + local id = get_id(sel_hf, sel_art) + self.frame_title = 'Finder'..(id ~= -1 and ' (#'..id..')' or '') + else + self.frame_title = 'Finder' + end +end + +function AdvFindWindow:onRenderFrame(dc, rect) + if not dfhack.world.isAdventureMode() then --Could be advfort, etc. + view:dismiss() + print('gui/adv-finder: lost adv mode, dismissing view') + end + self.super.onRenderFrame(self, dc, rect) + + local adv_panel = self.subviews.adv_panel + local target_panel = self.subviews.target_panel + + local target_data + if sel_hf >= 0 then --HF + local target_hf = findHF(sel_hf) + target_data = get_hf_data(target_hf) + target_panel.subviews.target_label:setText(hf_text(target_hf, target_data)) + elseif sel_art >= 0 then --Artifact + local target_art = df.artifact_record.find(sel_art) + target_data = get_art_data(target_art) + target_panel.subviews.target_label:setText(art_text(target_art, target_data)) + else --None + target_panel.subviews.target_label:setText() + end + adv_panel.subviews.adv_label:setText(adv_text(get_adv_data(), target_data)) + + adv_panel:updateLayout() + target_panel:updateLayout() + set_title(self) +end + +------------------- +-- AdvFindScreen -- +------------------- + +AdvFindScreen = defclass(AdvFindScreen, gui.ZScreen) +AdvFindScreen.ATTRS{ + focus_path = 'advfinder', +} + +function AdvFindScreen:init() + self:addviews{AdvFindWindow{}, AdvSelWindow{}} +end + +function AdvFindScreen:onDismiss() + view = nil +end + +if dfhack_flags.module then + return +end + +if not dfhack.world.isAdventureMode() then + qerror('Adventure mode only!') +end + +dfhack.onStateChange['adv-finder'] = function(sc) + if sc == SC_WORLD_UNLOADED then --Data is world-specific + sel_hf = -1 --Invalidate IDs + sel_art = -1 + filter_text = nil --Probably unwanted + cur_tab = 1 --Reset to first tab, but keep other settings + print('gui/adv-finder: cleared target') + dfhack.onStateChange['adv-finder'] = nil --Do once + end +end + +argparse.processArgsGetopt({...}, { + {'h', 'histfig', handler = function(arg) + sel_hf = math.tointeger(arg) or -1 + sel_art = -1 + end, hasArg = true}, + {'a', 'artifact', handler = function(arg) + sel_art = math.tointeger(arg) or -1 + sel_hf = -1 + end, hasArg = true}, + {'d', 'debug', handler = function() debug_id = true end}, +}) + +view = view and view:raise() or AdvFindScreen{}:show() From 1552b528739c0a78b28005eea9cd7e586ff8c812 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 7 Mar 2025 15:36:53 -0800 Subject: [PATCH 470/811] add adventure tag --- docs/gui/journal.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/journal.rst b/docs/gui/journal.rst index d773e2f863..5240002c5c 100644 --- a/docs/gui/journal.rst +++ b/docs/gui/journal.rst @@ -3,7 +3,7 @@ gui/journal .. dfhack-tool:: :summary: Fort journal with a multi-line text editor. - :tags: fort interface + :tags: adventure fort interface The `gui/journal` interface makes it easy to take notes and document important details for your fortresses and adventurers. From ab2a0ed72e23088412a8372346648993b154aedb Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 7 Mar 2025 16:11:06 -0800 Subject: [PATCH 471/811] changelog reorg --- changelog.txt | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index cef1f1948a..4c2684d404 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,17 +30,20 @@ Template for new versions: - `devel/export-map`: export map tile data to a JSON file - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk - `gui/spectate`: interactive UI for configuring `spectate` -- `launch`: (reinstated) thrash your enemies with a flying suplex -- `putontable`: (reinstated) make an item appear on a table like in adventure mode +- `launch`: (reinstated) new adventurer fighting move: thrash your enemies with a flying suplex +- `putontable`: (reinstated) make an item appear on a table +- `devel/query`: support adventure mode +- `devel/tree-info`: support adventure mode +- `hfs-pit`: support adventure mode +- `colonies`: support adventure mode +- `toggle-kbd-cursor`: support adventure mode ## New Features - `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys ## Fixes - `position`: support for adv mode look cursor -- `devel/query`, `devel/tree-info`, `hfs-pit`, `colonies`, `toggle-kbd-cursor`: now function in adventure mode -- `hfs-pit`: fix up tiletypes of pit walls, better placement of stairs (w/r/t eerie pits and ramp tops) -- `modtools/create-item`: ``hackWish`` now respects ``opts.pos`` and will spawn items there if provided +- `hfs-pit`: use correct wall types when making pits with walls ## Misc Improvements - `hide-tutorials`: handle tutorial popups for adventure mode @@ -49,6 +52,8 @@ Template for new versions: - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete - `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. - `gui/create-item`: now accepts a ``pos`` argument of where to spawn items +- `modtools/create-item`: exported ``hackWish`` function now supports ``opts.pos`` for determining spawn location +- `hfs-pit`: improve placement of stairs w/r/t eerie pits and ramp tops ## Removed From ecb2f96257249cf495cebc93eb29b0e00e087e94 Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 7 Mar 2025 16:11:44 -0800 Subject: [PATCH 472/811] don't change type of var --- devel/tree-info.lua | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/devel/tree-info.lua b/devel/tree-info.lua index d4d23b0c55..ec59563de0 100644 --- a/devel/tree-info.lua +++ b/devel/tree-info.lua @@ -173,11 +173,9 @@ function printTree(t) end if not dfhack_flags.module then - local p = guidm.getCursorPos() - if not p then - qerror('No cursor!') - end - p = dfhack.maps.getPlantAtTile(p) + local pos = guidm.getCursorPos() + if not pos then qerror('No cursor!') end + local p = dfhack.maps.getPlantAtTile(pos) if p and p.tree_info then printTree(p.tree_info) else From c5d2495ba28d7493db8c35e508d9c07086aa149b Mon Sep 17 00:00:00 2001 From: Myk Date: Fri, 7 Mar 2025 16:12:02 -0800 Subject: [PATCH 473/811] reword toggle-kbd-cursor docs --- docs/toggle-kbd-cursor.rst | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/docs/toggle-kbd-cursor.rst b/docs/toggle-kbd-cursor.rst index 4ae0e588d7..3f5b3f2996 100644 --- a/docs/toggle-kbd-cursor.rst +++ b/docs/toggle-kbd-cursor.rst @@ -5,12 +5,15 @@ toggle-kbd-cursor :summary: Toggles the keyboard cursor. :tags: adventure fort interface -This tool simply toggles the keyboard cursor so you can quickly switch it on -when you need it. Many other tools, like `autodump`, need a keyboard cursor for -selecting a target tile. Note that you'll still need to enter an interface mode -where the keyboard cursor is visible, like mining mode or dumping mode, in -order to use the cursor. In adventure mode, this tool toggles look mode via -simulated input. +This tool toggles the keyboard cursor so you can quickly switch it on when +you need it. Many other DFHack tools, like `autodump` or `digv`, need a +keyboard cursor for selecting a target tile. + +In fort mode, it toggles the game setting for "Enable keyboard cursor". +Note that you'll still need to enter an interface mode where the keyboard +cursor is visible, like mining mode or dumping mode. + +In adventure mode, it will toggle "Look" mode. Usage ----- From 2502d46e7a1b766ec48d50995cac3533189d4b7b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Mar 2025 16:45:10 -0800 Subject: [PATCH 474/811] light cleanup --- colonies.lua | 21 +-------------------- devel/light.lua | 11 ----------- docs/colonies.rst | 2 +- gui/companion-order.lua | 24 ------------------------ launch.lua | 7 ------- teleport.lua | 25 ------------------------- 6 files changed, 2 insertions(+), 88 deletions(-) diff --git a/colonies.lua b/colonies.lua index 48b8094d83..2672b66ab9 100644 --- a/colonies.lua +++ b/colonies.lua @@ -1,25 +1,6 @@ -- List, create, or change wild colonies (eg honey bees) -- By PeridexisErrant and Warmist -local help = [====[ - -colonies -======== -List vermin colonies, place honey bees, or convert all vermin -to honey bees. Usage: - -:colonies: List all vermin colonies on the map. -:colonies place: Place a honey bee colony under the cursor. -:colonies convert: Convert all existing colonies to honey bees. - -The ``place`` and ``convert`` subcommands by default create or -convert to honey bees, as this is the most commonly useful. -However both accept an optional flag to use a different vermin -type, for example ``colonies place ANT`` creates an ant colony -and ``colonies convert TERMITE`` ends your beekeeping industry. - -]====] - local guidm = require('gui.dwarfmode') function findVermin(target_verm) @@ -71,7 +52,7 @@ local args = {...} local target_verm = args[2] or "HONEY_BEE" if args[1] == 'help' or args[1] == '?' then - print(help) + print(dfhack.script_help()) elseif args[1] == 'convert' then convert_vermin_to(target_verm) elseif args[1] == 'place' then diff --git a/devel/light.lua b/devel/light.lua index 3786a26976..ff56e34c30 100644 --- a/devel/light.lua +++ b/devel/light.lua @@ -1,16 +1,5 @@ -- an experimental lighting engine ---[====[ -devel/light -=========== -An experimental lighting engine for DF, using the `rendermax` plugin. - -Call ``devel/light static`` to not recalculate lighting when in game. -Press :kbd:`~` to recalculate lighting. Press :kbd:`\`` to exit. - -]====] - -local gui = require 'gui' local guidm = require 'gui.dwarfmode' local render = require 'plugins.rendermax' diff --git a/docs/colonies.rst b/docs/colonies.rst index e778692162..cebdc69db6 100644 --- a/docs/colonies.rst +++ b/docs/colonies.rst @@ -3,7 +3,7 @@ colonies .. dfhack-tool:: :summary: Manipulate vermin colonies and hives. - :tags: fort armok map + :tags: adventure fort armok map Usage ----- diff --git a/gui/companion-order.lua b/gui/companion-order.lua index 814b4d9d8a..310bf5d194 100644 --- a/gui/companion-order.lua +++ b/gui/companion-order.lua @@ -1,28 +1,4 @@ -- Issue orders to companions in Adventure mode ---[====[ - -gui/companion-order -=================== -A script to issue orders for companions. Select companions with lower case chars (green when selected), issue orders with upper -case. Must be in look or talk mode to issue command on tile (e.g. move/equip/pick-up). - -.. image:: /docs/images/companion-order.png - -* move - orders selected companions to move to location. If companions are following they will move no more than 3 tiles from you. -* equip - try to equip items on the ground. -* pick-up - try to take items into hand (also wield) -* unequip - remove and drop equipment -* unwield - drop held items -* wait - temporarily remove from party -* follow - rejoin the party after "wait" -* leave - remove from party (can be rejoined by talking) - -Can be called with '-c' flag to display "cheating" commands. - -* patch up - fully heals the companion -* get in - rides e.g. minecart at cursor. Bit buggy as unit will teleport to the item when e.g. pushing it. - -]====] local gui = require 'gui' local guidm = require 'gui.dwarfmode' diff --git a/launch.lua b/launch.lua index 40aaf26995..942dde409e 100644 --- a/launch.lua +++ b/launch.lua @@ -1,13 +1,6 @@ -- Launch unit to cursor location -- Based on propel.lua by Roses, molested by Rumrusher and I until this happened, sorry. ---[====[ -launch -====== -Activate with a cursor on screen and you will go there rapidly. Attack -something first to ride them there. - -]====] local guidm = require('gui.dwarfmode') function launch(unitSource,unitRider) diff --git a/teleport.lua b/teleport.lua index 108f7e8673..8c7f052d38 100644 --- a/teleport.lua +++ b/teleport.lua @@ -2,31 +2,6 @@ -- author Putnam -- edited by expwnent --@module = true ---[====[ - -teleport -======== -Teleports a unit to given coordinates. - -.. note:: - - `gui/teleport` is an in-game UI for this script. - -Examples: - -* prints ID of unit beneath cursor:: - - teleport -showunitid - -* prints coordinates beneath cursor:: - - teleport -showpos - -* teleports unit ``1234`` to ``56,115,26`` - - teleport -unit 1234 -x 56 -y 115 -z 26 - -]====] local guidm = require('gui.dwarfmode') From c377890be62c944d5217422fc7930536268aa8d6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Mar 2025 17:11:47 -0800 Subject: [PATCH 475/811] update portrait, sprite, and labors when converting to adult --- changelog.txt | 2 ++ rejuvenate.lua | 5 +++++ 2 files changed, 7 insertions(+) diff --git a/changelog.txt b/changelog.txt index 0b46206fd2..0d42ca183c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -48,6 +48,8 @@ Template for new versions: - `hfs-pit`: use correct wall types when making pits with walls - `gui/liquids`: using the remove tool with magma selected will no longer create unexpected unpathable tiles - `idle-crafting`: do not assign crafting jobs to nobles holding meetings (avoid dangling jobs) +- `rejuvenate`: update unit portrait and sprite when aging up babies and children +- `rejuvenate`: recalculate labor assignments for unit when aging up babies and children (so they can start accepting jobs) ## Misc Improvements - `hide-tutorials`: handle tutorial popups for adventure mode diff --git a/rejuvenate.lua b/rejuvenate.lua index a2e36c86e2..ac0cf84f48 100644 --- a/rejuvenate.lua +++ b/rejuvenate.lua @@ -71,6 +71,11 @@ function rejuvenate(unit, quiet, force, dry_run, age) unit.profession2 = df.profession.STANDARD if hf then hf.profession = df.profession.STANDARD end end + unit.flags4.portrait_must_be_refreshed = true + unit.flags4.any_texture_must_be_refreshed = true + if dfhack.world.isFortressMode() then + dfhack.units.setAutomaticProfessions(unit) + end if not quiet then print(name .. ' is now ' .. age .. ' years old and will live a normal lifespan henceforth') end From 43a4e221ed1144b918a3c37ee44994cd55a8c6a6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 7 Mar 2025 17:40:42 -0800 Subject: [PATCH 476/811] reword changelog --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 0d42ca183c..9d95cb3c5a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,7 +37,7 @@ Template for new versions: - `devel/tree-info`: support adventure mode - `hfs-pit`: support adventure mode - `colonies`: support adventure mode -- `toggle-kbd-cursor`: support adventure mode +- `toggle-kbd-cursor`: support adventure mode (Alt-k keybinding now toggles Look mode) ## New Features - `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys From abff16ccda2113f864cc3bf613e52202d6ef5335 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 8 Mar 2025 00:00:33 -0800 Subject: [PATCH 477/811] don't add liquids to wall tiles --- changelog.txt | 1 + gui/liquids.lua | 64 ++++++++++++++++++++++++++----------------------- 2 files changed, 35 insertions(+), 30 deletions(-) diff --git a/changelog.txt b/changelog.txt index 9d95cb3c5a..dd2bdd70da 100644 --- a/changelog.txt +++ b/changelog.txt @@ -50,6 +50,7 @@ Template for new versions: - `idle-crafting`: do not assign crafting jobs to nobles holding meetings (avoid dangling jobs) - `rejuvenate`: update unit portrait and sprite when aging up babies and children - `rejuvenate`: recalculate labor assignments for unit when aging up babies and children (so they can start accepting jobs) +- `gui/liquids`: don't add liquids to wall tiles ## Misc Improvements - `hide-tutorials`: handle tutorial popups for adventure mode diff --git a/gui/liquids.lua b/gui/liquids.lua index bc89747edc..858af75f20 100644 --- a/gui/liquids.lua +++ b/gui/liquids.lua @@ -163,39 +163,43 @@ function SpawnLiquid:decreaseLiquidLevel() self.level = math.max(self.level - 1, 1) end +local function isFlowPassable(pos) + local tt = dfhack.maps.getTileType(pos) + local tile = dfhack.maps.getTileFlags(pos) + return tt and tile and df.tiletype_shape.attrs[df.tiletype.attrs[tt].shape].passable_flow +end + function SpawnLiquid:spawn(pos) - if dfhack.maps.isValidTilePos(pos) and dfhack.maps.isTileVisible(pos) then - local map_block = dfhack.maps.getTileBlock(pos) - - if self.mode == SpawnLiquidMode.CLEAN then - local tile = dfhack.maps.getTileFlags(pos) - - tile.water_salt = false - tile.water_stagnant = false - elseif self.type == df.tiletype.RiverSource then - if self.mode == SpawnLiquidMode.REMOVE then - local commands = { - 'f', 'any', ';', - 'f', 'sp', 'river_source', ';', - 'p', 'any', ';', - 'p', 's', 'floor', ';', - 'p', 'sp', 'normal', ';', - 'p', 'm', 'stone', ';', - } - dfhack.run_command('tiletypes-command', table.unpack(commands)) - dfhack.run_command('tiletypes-here', '--quiet', ('--cursor=%d,%d,%d'):format(pos2xyz(pos))) - liquids.spawnLiquid(pos, 0, df.tile_liquid.Water) - else - map_block.tiletype[pos.x % 16][pos.y % 16] = df.tiletype.RiverSource - liquids.spawnLiquid(pos, 7, df.tile_liquid.Water) - end + if not dfhack.maps.isValidTilePos(pos) or not dfhack.maps.isTileVisible(pos) or not isFlowPassable(pos) then + return + end + + local map_block = dfhack.maps.getTileBlock(pos) + + if self.mode == SpawnLiquidMode.CLEAN then + local tile = dfhack.maps.getTileFlags(pos) + + tile.water_salt = false + tile.water_stagnant = false + elseif self.type == df.tiletype.RiverSource then + if self.mode == SpawnLiquidMode.REMOVE then + local commands = { + 'f', 'any', ';', + 'f', 'sp', 'river_source', ';', + 'p', 'any', ';', + 'p', 's', 'floor', ';', + 'p', 'sp', 'normal', ';', + 'p', 'm', 'stone', ';', + } + dfhack.run_command('tiletypes-command', table.unpack(commands)) + dfhack.run_command('tiletypes-here', '--quiet', ('--cursor=%d,%d,%d'):format(pos2xyz(pos))) + liquids.spawnLiquid(pos, 0, df.tile_liquid.Water) else - liquids.spawnLiquid(pos, self:getLiquidLevel(pos), self.type) + map_block.tiletype[pos.x % 16][pos.y % 16] = df.tiletype.RiverSource + liquids.spawnLiquid(pos, 7, df.tile_liquid.Water) end - - -- Regardless of spawning or removing liquids, we need to reindex to - -- ensure pathability is up to date. - df.global.world.reindex_pathfinding = true + else + liquids.spawnLiquid(pos, self:getLiquidLevel(pos), self.type) end end From 2f1873d55fb4b7780fa02acdb0ff30569551b0d1 Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 8 Mar 2025 17:30:44 +0800 Subject: [PATCH 478/811] emigrate nobles (#1382) * Add emigrate-nobles.lua and rst * Add logic for finding noble's sites * Add monarch handling logic * Add emigration logic * Add announcement * Add options handling * Add documentation * Attempt to fix units stuck in social activities * Fix title underline * Simplify logic * Update docs/emigrate-nobles.rst Co-authored-by: Myk * Update docs/emigrate-nobles.rst Co-authored-by: Myk * Update docs/emigrate-nobles.rst Co-authored-by: Myk * Update docs/emigrate-nobles.rst * Add functionality to remove from squad * Remove squad removal logic * Remove unused function * Move emigrate-nobles into emigration * Move emigrate-nobles.rst into emigration.rst * Update docs/emigration.rst Co-authored-by: Myk * Update internal/emigration/emigrate-nobles.lua Co-authored-by: Myk * Remove module globals * Fix pair iteration * Refactor logic * Refactor emigration logic * Fix stupid typo * Fix stupid typo * Fix import error * Add support for current unit * Disable cancelling special jobs * Add mandate removal logic * Add check for fort admins * Add signposts for navigation * Change isAdministrator * Add support for other civ monarchs, change print messages * Add support for evicting noble mayors * Add symbol dropping logic * Update TODO * Update docs/emigration.rst * Fix bad mayor assignment logic * Update for new structures, add missing event * Fix incorrect history event * Update styling of new links and events * Update changelog.txt * doc edits --------- Co-authored-by: Myk --- changelog.txt | 1 + docs/emigration.rst | 35 +++ emigration.lua | 111 ++------ internal/emigration/emigrate-nobles.lua | 346 ++++++++++++++++++++++++ internal/emigration/unit-link-utils.lua | 257 ++++++++++++++++++ 5 files changed, 657 insertions(+), 93 deletions(-) create mode 100644 internal/emigration/emigrate-nobles.lua create mode 100644 internal/emigration/unit-link-utils.lua diff --git a/changelog.txt b/changelog.txt index dd2bdd70da..be7c87b4c5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -42,6 +42,7 @@ Template for new versions: ## New Features - `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys - `gui/journal`: now working in adventure mode +- `emigration`: new ``nobles`` command for sending "freeloader" barons back to the sites that they rule over ## Fixes - `position`: support for adv mode look cursor diff --git a/docs/emigration.rst b/docs/emigration.rst index 58a3d623e7..074c8ebc96 100644 --- a/docs/emigration.rst +++ b/docs/emigration.rst @@ -16,9 +16,44 @@ even in the company of a visiting elven bard! The check is made monthly. A happy dwarf (i.e. with negative stress) will never emigrate. +The tool also supports ``nobles``, a manually-invoked command that makes nobles +emigrate to their rightful land of rule. No more freeloaders making inane demands! +Nobles assigned to squads or to fort administrator positions will not be emigrated. +Remove their assignments before retrying. Nobles holding elected positions +(i.e. mayors) may be emigrated, but will be marked with a ``*`` icon when listed. + Usage ----- :: enable emigration + emigration nobles [--list] + emigration nobles [] + +Examples +-------- + +``emigration nobles`` + Emigrate the selected noble if it does not rule your fortress. + If no unit is selected, list all nobles that do not rule your fortress. +``emigration nobles --list`` + List all nobles that do not rule your fortress. Nobles that cannot be emigrated + (see above) will have a ``!`` indicator while nobles holding elected positions + will have a ``*`` indicator. +``emigration nobles --all`` + Emigrate all nobles that do not rule your fortress. +``emigration nobles --unit 34534`` + Emigrate a noble matching the specified unit ID that does not rule your fortress. + +Options +------- + +These options are exclusive to the ``emigration nobles`` command. + +``-l``, ``--list`` + List all nobles that do not rule your fortress +``-a``, ``--all`` + Emigrate all nobles do not rule your fortress +``-u``, ``--unit `` + Emigrate noble matching specified unit ID that does not rule your fortress diff --git a/emigration.lua b/emigration.lua index ea494a7e36..1bba9810e1 100644 --- a/emigration.lua +++ b/emigration.lua @@ -3,6 +3,9 @@ local utils = require('utils') +local nobles = reqscript('internal/emigration/emigrate-nobles') +local unit_link_utils = reqscript('internal/emigration/unit-link-utils') + local GLOBAL_KEY = 'emigration' -- used for state change hooks and persistence local function get_default_state() @@ -37,121 +40,40 @@ function desert(u,method,civ) local line = dfhack.units.getReadableName(u) .. " has " if method == 'merchant' then line = line.."joined the merchants" - u.flags1.merchant = true - u.civ_id = civ + unit_link_utils.markUnitForEmigration(u, civ, false) else line = line.."abandoned the settlement in search of a better life." - u.civ_id = civ - u.flags1.forest = true - u.flags2.visitor = true - u.animal.leave_countdown = 2 + unit_link_utils.markUnitForEmigration(u, civ, true) end - local hf_id = u.hist_figure_id + local hf = df.historical_figure.find(u.hist_figure_id) local fort_ent = df.global.plotinfo.main.fortress_entity local civ_ent = df.historical_entity.find(hf.civ_id) local newent_id = -1 local newsite_id = -1 - -- free owned rooms - for i = #u.owned_buildings-1, 0, -1 do - local temp_bld = df.building.find(u.owned_buildings[i].id) - dfhack.buildings.setOwner(temp_bld, nil) - end - - -- remove from workshop profiles - for _, bld in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do - for k, v in ipairs(bld.profile.permitted_workers) do - if v == u.id then - bld.profile.permitted_workers:erase(k) - break - end - end - end - for _, bld in ipairs(df.global.world.buildings.other.FURNACE_ANY) do - for k, v in ipairs(bld.profile.permitted_workers) do - if v == u.id then - bld.profile.permitted_workers:erase(k) - break - end - end - end - - -- disassociate from work details - for _, detail in ipairs(df.global.plotinfo.labor_info.work_details) do - for k, v in ipairs(detail.assigned_units) do - if v == u.id then - detail.assigned_units:erase(k) - break - end - end - end - - -- unburrow - for _, burrow in ipairs(df.global.plotinfo.burrows.list) do - dfhack.burrows.setAssignedUnit(burrow, u, false) - end - - -- erase the unit from the fortress entity - for k,v in ipairs(fort_ent.histfig_ids) do - if v == hf_id then - df.global.plotinfo.main.fortress_entity.histfig_ids:erase(k) - break - end - end - for k,v in ipairs(fort_ent.hist_figures) do - if v.id == hf_id then - df.global.plotinfo.main.fortress_entity.hist_figures:erase(k) - break - end - end - for k,v in ipairs(fort_ent.nemesis) do - if v.figure.id == hf_id then - df.global.plotinfo.main.fortress_entity.nemesis:erase(k) - df.global.plotinfo.main.fortress_entity.nemesis_ids:erase(k) - break - end - end - - -- remove the old entity link and create new one to indicate former membership - hf.entity_links:insert("#", {new = df.histfig_entity_link_former_memberst, entity_id = fort_ent.id, link_strength = 100}) - for k,v in ipairs(hf.entity_links) do - if v._type == df.histfig_entity_link_memberst and v.entity_id == fort_ent.id then - hf.entity_links:erase(k) - break - end - end + unit_link_utils.removeUnitAssociations(u) + unit_link_utils.removeHistFigFromEntity(hf, fort_ent) -- try to find a new entity for the unit to join - for k,v in ipairs(civ_ent.entity_links) do - if v.type == df.entity_entity_link_type.CHILD and v.target ~= fort_ent.id then - newent_id = v.target + for _,entity_link in ipairs(civ_ent.entity_links) do + if entity_link.type == df.entity_entity_link_type.CHILD and entity_link.target ~= fort_ent.id then + newent_id = entity_link.target break end end if newent_id > -1 then - hf.entity_links:insert("#", {new = df.histfig_entity_link_memberst, entity_id = newent_id, link_strength = 100}) - -- try to find a new site for the unit to join - for k,v in ipairs(df.global.world.entities.all[hf.civ_id].site_links) do + for _,site_link in ipairs(df.global.world.entities.all[hf.civ_id].site_links) do local site_id = df.global.plotinfo.site_id - if v.type == df.entity_site_link_type.Claim and v.target ~= site_id then - newsite_id = v.target + if site_link.type == df.entity_site_link_type.Claim and site_link.target ~= site_id then + newsite_id = site_link.target break end end local newent = df.historical_entity.find(newent_id) - newent.histfig_ids:insert('#', hf_id) - newent.hist_figures:insert('#', hf) - local hf_event_id = df.global.hist_event_next_id - df.global.hist_event_next_id = df.global.hist_event_next_id+1 - df.global.world.history.events:insert("#", {new = df.history_event_add_hf_entity_linkst, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, civ = newent_id, histfig = hf_id, link_type = 0}) - if newsite_id > -1 then - local hf_event_id = df.global.hist_event_next_id - df.global.hist_event_next_id = df.global.hist_event_next_id+1 - df.global.world.history.events:insert("#", {new = df.history_event_change_hf_statest, year = df.global.cur_year, seconds = df.global.cur_year_tick, id = hf_event_id, hfid = hf_id, state = 1, reason = -1, site = newsite_id}) - end + unit_link_utils.addHistFigToSite(hf, newsite_id, newent) end print(dfhack.df2console(line)) dfhack.gui.showAnnouncement(line, COLOR_WHITE) @@ -251,6 +173,9 @@ if args[1] == "enable" then state.enabled = true elseif args[1] == "disable" then state.enabled = false +elseif args[1] == "nobles" then + table.remove(args, 1) + nobles.run(args) else print('emigration is ' .. (state.enabled and 'enabled' or 'not enabled')) return diff --git a/internal/emigration/emigrate-nobles.lua b/internal/emigration/emigrate-nobles.lua new file mode 100644 index 0000000000..f4ef605de0 --- /dev/null +++ b/internal/emigration/emigrate-nobles.lua @@ -0,0 +1,346 @@ +--@module = true + +--[[ +TODO: + * Feature: have rightful ruler immigrate to fort if off-site +]]-- + +local argparse = require("argparse") + +local unit_link_utils = reqscript("internal/emigration/unit-link-utils") + +local options = { + all = false, + unitId = -1, + list = false +} + +-- adapted from Units::get_land_title() +---@return df.world_site|nil +local function findSiteOfRule(np) + local site = nil + local civ = np.entity -- lawmakers seem to be all civ-level positions + for _, link in ipairs(civ.site_links) do + if not link.flags.land_for_holding then goto continue end + if link.position_profile_id ~= np.assignment.id then goto continue end + + site = df.world_site.find(link.target) + break + ::continue:: + end + + return site +end + +---@return df.world_site|nil +local function findCapital(civ) + local civCapital = nil + for _, link in ipairs(civ.site_links) do + if link.flags.capital then + civCapital = df.world_site.find(link.target) + break + end + end + + return civCapital +end + +---@param unit df.unit +---@param nobleList { unit: df.unit, site: df.world_site, id: number }[] +---@param thisSite df.world_site +---@param civ df.historical_entity +local function addNobleOfOtherSite(unit, nobleList, thisSite, civ) + local nps = dfhack.units.getNoblePositions(unit) or {} + local noblePos = nil + for _, np in ipairs(nps) do + if np.position.flags.IS_LAW_MAKER then + noblePos = np + break + end + end + + if not noblePos then return end -- unit is not nobility + + -- Monarchs do not seem to have an world_site associated to them (?) + if noblePos.position.flags.RULES_FROM_LOCATION and noblePos.entity.id == civ.id then + local capital = findCapital(civ) + if capital and capital.id ~= thisSite.id then + table.insert(nobleList, {unit = unit, site = capital, id = noblePos.assignment.id}) + end + return + end + + local name = dfhack.units.getReadableName(unit) + -- Logic for dukes, counts, barons + local site = findSiteOfRule(noblePos) + if not site then qerror("could not find land of "..name) end + + if site.id == thisSite.id then return end -- noble rules current fort + table.insert(nobleList, {unit = unit, site = site, id = noblePos.assignment.id}) +end + +---@param unit df.unit +local function removeMandates(unit) + local mandates = df.global.world.mandates.all + for i=#mandates-1,0,-1 do + local mandate = mandates[i] + if mandate.unit and mandate.unit.id == unit.id then + mandates:erase(i) + mandate:delete() + end + end +end + +-- adapted from emigration::desert() +---@param unit df.unit +---@param toSite df.world_site +---@param prevEnt df.historical_entity +---@param civ df.historical_entity +---@param removeMayor boolean +local function emigrate(unit, toSite, prevEnt, civ, removeMayor) + local histFig = df.historical_figure.find(unit.hist_figure_id) + if not histFig then + print("Could not find associated historical figure!") + return + end + + unit_link_utils.markUnitForEmigration(unit, civ.id, true) + + -- remove current job + if unit.job.current_job then dfhack.job.removeJob(unit.job.current_job) end + + -- break up any social activities + for _, actId in ipairs(unit.social_activities) do + local act = df.activity_entry.find(actId) + if act then act.events[0].flags.dismissed = true end + end + + -- cancel any associated mandates + removeMandates(unit) + + unit_link_utils.removeUnitAssociations(unit) + unit_link_utils.removeHistFigFromEntity(histFig, prevEnt, removeMayor) + + -- have unit join new site government + local siteGov = df.historical_entity.find(toSite.cur_owner_id) + if not siteGov then qerror("could not find entity associated with new site") end + unit_link_utils.addHistFigToSite(histFig, toSite.id, siteGov) + + -- announce the changes + local unitName = dfhack.df2console(dfhack.units.getReadableName(unit)) + local siteName = dfhack.df2console(dfhack.translation.translateName(toSite.name, true)) + local govName = dfhack.df2console(dfhack.translation.translateName(siteGov.name, true)) + local line = unitName .. " has left to join " ..govName.. " as lord of " .. siteName .. "." + print("+ "..dfhack.df2console(line)) + dfhack.gui.showAnnouncement(line, COLOR_WHITE) +end + +------------------------ +-- [[ GUARD CHECKS ]] -- +------------------------ + +---@param unit df.unit +local function inSpecialJob(unit) + local job = unit.job.current_job + if not job then return false end + + if job.flags.special then return true end -- cannot cancel + + local jobType = job.job_type -- taken from notifications::for_moody() + return df.job_type_class[df.job_type.attrs[jobType].type] == 'StrangeMood' +end + +---@param unit df.unit +local function isSoldier(unit) + return unit.military.squad_id ~= -1 +end + +-- just an enum +local AdminType = { + NOT_ADMIN = { sym = " " }, + IS_ELECTED = { sym = "*" }, + IS_ADMIN = { sym = "!" } +} + +---@param unit df.unit +---@param fortEnt df.historical_entity +local function getAdminType(unit, fortEnt) + ---@diagnostic disable-next-line: missing-parameter + local nps = dfhack.units.getNoblePositions(unit) or {} + local result = AdminType.NOT_ADMIN + + ---@diagnostic disable-next-line: param-type-mismatch + for _, np in ipairs(nps) do + if np.entity.id ~= fortEnt.id then goto continue end + if np.position.flags.ELECTED then + result = AdminType.IS_ELECTED + goto continue + end + + -- Mayors cannot be evicted if they are also appointed administrators (e.g. manager) + result = AdminType.IS_ADMIN + break + ::continue:: + end + return result +end + +----------------------- +-- [[ PRINT MODES ]] -- +----------------------- + +---@param nobleList { unit: df.unit, site: df.world_site }[] +---@param fortEnt df.historical_entity +local function listNoblesFound(nobleList, fortEnt) + for _, record in ipairs(nobleList) do + local unit = record.unit + local site = record.site + + -- avoid scoping errors + local adminType = nil + local siteName = "" + + local nobleName = dfhack.units.getReadableName(unit) + local unitMsg = unit.id..": "..nobleName + if isSoldier(unit) then + local squad = df.squad.find(unit.military.squad_id) + local squadName = squad + and dfhack.translation.translateName(squad.name, true) + or "unknown squad" + + unitMsg = "! "..unitMsg.." - soldier in "..squadName + goto print + end + + adminType = getAdminType(unit, fortEnt) + if adminType ~= AdminType.NOT_ADMIN then + local status = adminType == AdminType.IS_ADMIN + and "fort administrator" -- isAdmin + or "elected official" -- isElected + unitMsg = adminType.sym.." "..unitMsg.." - "..status + goto print + end + + siteName = dfhack.translation.translateName(site.name, true) + unitMsg = " "..unitMsg.." - to "..siteName + + ::print:: + print(unitMsg) + end +end + +local function printNoNobles() + if options.unitId == -1 then + print("No eligible nobles to be emigrated.") + else + print("Unit ID "..options.unitId.." is not an eligible noble.") + end +end + +------------------------- +-- [[ MAIN FUNCTION ]] -- +------------------------- + +local function main() + ---@diagnostic disable-next-line: assign-type-mismatch + local fort = dfhack.world.getCurrentSite() ---@type df.world_site + if not fort then qerror("could not find current site") end + + local fortEnt = df.global.plotinfo.main.fortress_entity + + local civ = df.historical_entity.find(df.global.plotinfo.civ_id) + if not civ then qerror("could not find current civ") end + + ---@type { unit: df.unit, site: df.world_site, id: number }[] + local freeloaders = {} + for _, unit in ipairs(dfhack.units.getCitizens()) do + if options.unitId ~= -1 and unit.id ~= options.unitId then goto continue end + + addNobleOfOtherSite(unit, freeloaders, fort, civ) + ::continue:: + end + + if #freeloaders == 0 then + printNoNobles() + return + end + + if options.list then + listNoblesFound(freeloaders, fortEnt) + return + end + + for _, record in ipairs(freeloaders) do + local noble = record.unit + local site = record.site + local adminType = nil + + local nobleName = dfhack.units.getReadableName(noble) + if inSpecialJob(noble) then + print("! "..nobleName.." is busy! Leave alone for now.") + goto continue + elseif isSoldier(noble) then + print("! "..nobleName.." is in a squad! Unassign unit and try again.") + goto continue + end + + adminType = getAdminType(noble, fortEnt) + if adminType == AdminType.IS_ADMIN then + print("! "..nobleName.." is an administrator! Unassign unit and try again.") + goto continue + end + + local isElected = adminType == AdminType.IS_ELECTED + emigrate(noble, site, fortEnt, civ, isElected) + unit_link_utils.unassignSymbols(record.id, civ, fort) + ::continue:: + end +end + +local function initChecks() + if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('needs a loaded fortress map') + end + + if options.list then return true end -- list option does not require unit options + + local noOptions = options.unitId == -1 and not options.all + if noOptions then + unit = dfhack.gui.getSelectedUnit(true) + if unit then + options.unitId = unit.id + local name = dfhack.units.getReadableName(unit) + print("Selecting "..name.." (ID "..unit.id..")") + else + options.list = true + print("Defaulting to list mode:") + end + + return true + end + + local invalidUnit = options.unitId ~= -1 and options.all + if invalidUnit then qerror("Either specify one unit or all.") end + + return true +end + +local function resetOptions() + options.all = false + options.unitId = -1 + options.list = false +end + +function run(args) + argparse.processArgsGetopt(args, { + {"a", "all", handler=function() options.all = true end}, + {"u", "unit", hasArg=true, handler=function(id) options.unitId = tonumber(id) end}, + {"l", "list", handler=function() options.list = true end} + }) + + if initChecks() then + main() + end + + resetOptions() +end diff --git a/internal/emigration/unit-link-utils.lua b/internal/emigration/unit-link-utils.lua new file mode 100644 index 0000000000..e3bc294d46 --- /dev/null +++ b/internal/emigration/unit-link-utils.lua @@ -0,0 +1,257 @@ +--@module = true + +---@param histFig df.historical_figure +---@param oldEntity df.historical_entity +local function unassignMayor(histFig, oldEntity) + local assignmentId = -1 + local positionId = -1 + local nps = dfhack.units.getNoblePositions(histFig) or {} + for _,pos in ipairs(nps) do + if pos.entity.id == oldEntity.id and pos.position.flags.ELECTED then + pos.assignment.histfig = -1 + pos.assignment.histfig2 = -1 + assignmentId = pos.assignment.id + positionId = pos.position.id + end + end + if assignmentId == -1 then qerror("could not find mayor assignment!") end + + local startYear = -1 -- remove mayor assignment + for k,v in ipairs(histFig.entity_links) do + if v.entity_id == oldEntity.id + and df.histfig_entity_link_positionst:is_instance(v) + and v.assignment_id == assignmentId + then + startYear = v.start_year + histFig.entity_links:erase(k) + v:delete() + break + end + end + if startYear == -1 then qerror("could not find entity link!") end + + histFig.entity_links:insert('#', { + new = df.histfig_entity_link_former_positionst, + assignment_id = assignmentId, + start_year = startYear, + entity_id = oldEntity.id, + end_year = df.global.cur_year, + link_strength = 100 + }) + + local hfEventId = df.global.hist_event_next_id + df.global.hist_event_next_id = df.global.hist_event_next_id+1 + df.global.world.history.events:insert("#", { + new = df.history_event_remove_hf_entity_linkst, + year = df.global.cur_year, + seconds = df.global.cur_year_tick, + id = hfEventId, + civ = oldEntity.id, + histfig = histFig.id, + link_type = df.histfig_entity_link_type.POSITION, + position_id = positionId + }) +end + +---@param histFig df.historical_figure +---@param oldEntity df.historical_entity +---@param removeMayor boolean +function removeHistFigFromEntity(histFig, oldEntity, removeMayor) + if not histFig or not oldEntity then return end + + local histFigId = histFig.id + + -- erase the unit from the fortress entity + for k,v in ipairs(oldEntity.histfig_ids) do + if v == histFigId then + df.global.plotinfo.main.fortress_entity.histfig_ids:erase(k) + break + end + end + for k,v in ipairs(oldEntity.hist_figures) do + if v.id == histFigId then + df.global.plotinfo.main.fortress_entity.hist_figures:erase(k) + break + end + end + for k,v in ipairs(oldEntity.nemesis) do + if v.figure.id == histFigId then + df.global.plotinfo.main.fortress_entity.nemesis:erase(k) + df.global.plotinfo.main.fortress_entity.nemesis_ids:erase(k) + break + end + end + + -- remove mayor assignment if exists + if removeMayor then unassignMayor(histFig, oldEntity) end + + -- remove the old entity link and create new one to indicate former membership + histFig.entity_links:insert("#", {new = df.histfig_entity_link_former_memberst, entity_id = oldEntity.id, link_strength = 100}) + for k,v in ipairs(histFig.entity_links) do + if v._type == df.histfig_entity_link_memberst and v.entity_id == oldEntity.id then + histFig.entity_links:erase(k) + break + end + end +end + +---Creates events indicating a histfig's move to a new site and joining its entity. +---@param histFig df.historical_figure +---@param siteId number Set to -1 if unneeded +---@param siteGov df.historical_entity +function addHistFigToSite(histFig, siteId, siteGov) + if not histFig or not siteGov then return nil end + + local histFigId = histFig.id + + -- add new site gov to histfig links + histFig.entity_links:insert("#", { + new = df.histfig_entity_link_memberst, + entity_id = siteGov.id, + link_strength = 100 + }) + + -- add histfig to new site gov + siteGov.histfig_ids:insert('#', histFigId) + siteGov.hist_figures:insert('#', histFig) + local hfEventId = df.global.hist_event_next_id + df.global.hist_event_next_id = df.global.hist_event_next_id+1 + df.global.world.history.events:insert("#", { + new = df.history_event_add_hf_entity_linkst, + year = df.global.cur_year, + seconds = df.global.cur_year_tick, + id = hfEventId, + civ = siteGov.id, + histfig = histFigId, + link_type = df.histfig_entity_link_type.MEMBER + }) + + if siteId <= -1 then return end -- skip site join event + + -- create event indicating histfig moved to site + hfEventId = df.global.hist_event_next_id + df.global.hist_event_next_id = df.global.hist_event_next_id+1 + df.global.world.history.events:insert("#", { + new = df.history_event_change_hf_statest, + year = df.global.cur_year, + seconds = df.global.cur_year_tick, + id = hfEventId, + hfid = histFigId, + state = df.whereabouts_type.settler, + reason = df.history_event_reason.none, + site = siteId + }) +end + +---@param unit df.unit +function removeUnitAssociations(unit) + -- free owned rooms + for i = #unit.owned_buildings-1, 0, -1 do + local tmp = df.building.find(unit.owned_buildings[i].id) + dfhack.buildings.setOwner(tmp, nil) + end + + -- remove from workshop profiles + for _, bld in ipairs(df.global.world.buildings.other.WORKSHOP_ANY) do + for k, v in ipairs(bld.profile.permitted_workers) do + if v == unit.id then + bld.profile.permitted_workers:erase(k) + break + end + end + end + for _, bld in ipairs(df.global.world.buildings.other.FURNACE_ANY) do + for k, v in ipairs(bld.profile.permitted_workers) do + if v == unit.id then + bld.profile.permitted_workers:erase(k) + break + end + end + end + + -- disassociate from work details + for _, detail in ipairs(df.global.plotinfo.labor_info.work_details) do + for k, v in ipairs(detail.assigned_units) do + if v == unit.id then + detail.assigned_units:erase(k) + break + end + end + end + + -- unburrow + for _, burrow in ipairs(df.global.plotinfo.burrows.list) do + dfhack.burrows.setAssignedUnit(burrow, unit, false) + end +end + +---@param unit df.unit +---@param civId number +---@param leaveNow boolean Decides if unit leaves immediately or with merchants +function markUnitForEmigration(unit, civId, leaveNow) + unit.following = nil + unit.civ_id = civId + + if leaveNow then + unit.flags1.forest = true + unit.flags2.visitor = true + unit.animal.leave_countdown = 2 + else + unit.flags1.merchant = true + end +end + +---@param item df.item +local function getPos(item) + local x, y, z = dfhack.items.getPosition(item) + if not x or not y or not z then + return nil + end + + if dfhack.maps.isTileVisible(x, y, z) then + return xyz2pos(x, y, z) + end +end + +---@param assignmentId number +---@param entity df.historical_entity +---@param site df.world_site +function unassignSymbols(assignmentId, entity, site) + local claims = entity.artifact_claims + local artifacts = df.global.world.artifacts.all + + for i=#claims-1,0,-1 do + local claim = claims[i] + if claim.claim_type ~= df.artifact_claim_type.Symbol then goto continue end + if claim.symbol_claim_id ~= assignmentId then goto continue end + + local artifact = artifacts[claim.artifact_id] + local item = artifact.item + local artifactName = dfhack.translation.translateName(artifact.name) + + -- we can probably keep artifact.entity_claims since we still hold it + local itemPos = getPos(item) + local success = false + if not itemPos then + if artifact.site == site.id then + print(" ! "..artifactName.." cannot be found!") + goto removeClaim + else + print(" ! "..artifactName.." is not in this site!") + goto continue + end + end + + success = dfhack.items.moveToGround(item, itemPos) + if success then print(" + dropped "..artifactName) + else print(" ! could not drop "..artifactName) + end + + -- they do not seem to "own" their artifacts, no additional cleaning seems necessary + + ::removeClaim:: + claims:erase(i) + claim:delete() + ::continue:: + end +end From 623cbcb1a28600e89da964eadd58c16a40da6da7 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sun, 9 Mar 2025 09:58:59 +0100 Subject: [PATCH 479/811] idle-crafting: default to only creating crafting jobs for happy units if they have strong needs --- changelog.txt | 3 +++ docs/idle-crafting.rst | 11 +++++++- idle-crafting.lua | 57 ++++++++++++++++++++++++++++++++++-------- 3 files changed, 59 insertions(+), 12 deletions(-) diff --git a/changelog.txt b/changelog.txt index be7c87b4c5..92e1d5e29b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -43,6 +43,8 @@ Template for new versions: - `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys - `gui/journal`: now working in adventure mode - `emigration`: new ``nobles`` command for sending "freeloader" barons back to the sites that they rule over +- `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys +- `idle-crafting`: default to only considering happy and ecstatic units for the highest need threshold ## Fixes - `position`: support for adv mode look cursor @@ -52,6 +54,7 @@ Template for new versions: - `rejuvenate`: update unit portrait and sprite when aging up babies and children - `rejuvenate`: recalculate labor assignments for unit when aging up babies and children (so they can start accepting jobs) - `gui/liquids`: don't add liquids to wall tiles +- `idle-crafting`: check that units still have crafting needs before creating a job for them ## Misc Improvements - `hide-tutorials`: handle tutorial popups for adventure mode diff --git a/docs/idle-crafting.rst b/docs/idle-crafting.rst index b0377a6a75..6eeb571e7f 100644 --- a/docs/idle-crafting.rst +++ b/docs/idle-crafting.rst @@ -26,6 +26,12 @@ Usage given unit. Units meeting higher thresholds will be prioritized. Defaults to ``500,1000,10000``. +``idle-crafting happy [yes|no]`` + If set to ``no``, "happy" and "ecstatic" dwarves not suffering from + long-term stress will only satisfy their crafting needs when meeting the + highest configured threshold (eg. ``10000`` at default settings). Defaults + to ``no``. + ``disable idle-crafting`` Disallow idle crafting at all workshops. You can re-enable idle crafting at individual Craftsdwarf's workshops. @@ -34,7 +40,10 @@ Examples -------- ``idle-crafting thresholds 500,1000,10000`` - Reset thresholds to defaults. + Reset thresholds to defaults. + +``idle-crafting happy yes`` + Treat happy and ecstatic dwarves the same as everyone else. Overlay ------- diff --git a/idle-crafting.lua b/idle-crafting.lua index d67f8122a2..33518f3522 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -235,6 +235,9 @@ watched = watched or {} ---@type integer[] thresholds = thresholds or { 10000, 1000, 500 } +---@type boolean +ignore_happy = ignore_happy == nil and true or ignore_happy + -- persisting a table with numeric keys results in a json array with a huge number of null entries -- therefore, we convert the keys to strings for persistence -- also, we clear the frame counter values since the frame counter gets reset on load @@ -262,7 +265,8 @@ local function persist_state() dfhack.persistent.saveSiteData(GLOBAL_KEY, { enabled=enabled, allowed=to_persist_allowed(), - thresholds=thresholds + thresholds=thresholds, + ignore_happy=ignore_happy }) end @@ -273,6 +277,7 @@ local function load_state() enabled = persisted_data.enabled or false allowed = from_persist_allowed(persisted_data.allowed) or {} thresholds = persisted_data.thresholds or { 10000, 1000, 500 } + ignore_happy = persisted_data.ignore_happy == nil and true or ignore_happy end --frequently accessed values @@ -281,16 +286,18 @@ local BONE_CARVE = df.unit_labor['BONE_CARVE'] local STONE_CRAFT = df.unit_labor['STONE_CRAFT'] ---negative crafting focus penalty +---@generic T ---@param unit df.unit ----@return number -function getCraftingNeed(unit) +---@param value_if_absent T +---@return number|T +function getCraftingNeed(unit, value_if_absent) local needs = unit.status.current_soul.personality.needs for _, need in ipairs(needs) do if need.id == CraftObject then return -need.focus_level end end - return 0 + return value_if_absent end local function stop() @@ -383,8 +390,8 @@ end ---@return boolean "proceed to next workshop" local function processUnit(workshop, idx, unit_id) local unit = df.unit.find(unit_id) - -- check that unit is still there and not caged or chained - if not unit or unit.flags1.caged or unit.flags1.chained then + -- check that unit is still there, not caged or chained, and still has crafting needs + if not unit or unit.flags1.caged or unit.flags1.chained or getCraftingNeed(unit, -1) < 0 then watched[idx][unit_id] = nil return false elseif not canAccessWorkshop(unit, workshop) then @@ -511,16 +518,33 @@ local function main_loop() num_watched[idx] = 0 end + local max_threshold = thresholds[1] for _, unit in ipairs(dfhack.units.getCitizens(true, false)) do + + local crafting_need = getCraftingNeed(unit, nil) + + if not crafting_need then + goto next_unit + end + + local is_happy = unit.status.current_soul.personality.stress < -25000 and + unit.status.current_soul.personality.longterm_stress < 0 + for idx, threshold in ipairs(thresholds) do - if getCraftingNeed(unit) > threshold then + if ignore_happy and is_happy and threshold < max_threshold then + -- ignore happy and ecstatic units for any threshold but the highest + print(dfhack.df2console(("idle-crafting: skipping happy unit %s"):format(dfhack.units.getReadableName(unit)))) + goto next_unit + end + + if crafting_need > threshold then watched[idx][unit.id] = true num_watched[idx] = num_watched[idx] + 1 watching = true - goto continue + goto next_unit end end - ::continue:: + ::next_unit:: end -- print(('watching %s dwarfs with crafting needs'):format( -- table.concat(num_watched, '/') @@ -665,9 +689,12 @@ if not positionals[1] or positionals[1] == 'status' then ---@type integer[] stats = {} for _, unit in ipairs(dfhack.units.getCitizens(true, false)) do - local fulfillment = -getCraftingNeed(unit) + local crafting_need = getCraftingNeed(unit, nil) + if not crafting_need then + goto continue + end for i = 1, 7 do - if fulfillment >= fulfillment_threshold[i] then + if -crafting_need >= fulfillment_threshold[i] then stats[i] = stats[i] and stats[i] + 1 or 1 goto continue end @@ -686,11 +713,19 @@ if not positionals[1] or positionals[1] == 'status' then format(enabled and 'enabled' or 'disabled', num_workshops)) print(('The thresholds for "craft item" needs are %s'): format(table.concat(thresholds, ','))) + if ignore_happy then + print('Will only assign crafting jobs to happy dwarves with strong needs') + else + print('Will treat happy units like all other units') + end + elseif positionals[1] == 'thresholds' then thresholds = argparse.numberList(positionals[2], 'thresholds') table.sort(thresholds, function (a, b) return a > b end) print(('Thresholds for "craft item" needs set to %s'): format(table.concat(thresholds, ','))) +elseif positionals[1] == 'happy' then + ignore_happy = not argparse.boolean(positionals[2], 'happy') elseif positionals[1] == 'disable' then allowed = {} stop() From d399dd322781b3f3c453d6d6c7f8cc196cdbffc5 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Tue, 11 Mar 2025 11:04:44 -0700 Subject: [PATCH 480/811] changelog editing pass --- changelog.txt | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/changelog.txt b/changelog.txt index be7c87b4c5..ca3fc9ac3b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,25 +33,20 @@ Template for new versions: - `gui/notes`: UI for adding and managing notes attached to tiles on the map - `launch`: (reinstated) new adventurer fighting move: thrash your enemies with a flying suplex - `putontable`: (reinstated) make an item appear on a table -- `devel/query`: support adventure mode -- `devel/tree-info`: support adventure mode -- `hfs-pit`: support adventure mode -- `colonies`: support adventure mode -- `toggle-kbd-cursor`: support adventure mode (Alt-k keybinding now toggles Look mode) ## New Features -- `advtools`: new ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys -- `gui/journal`: now working in adventure mode -- `emigration`: new ``nobles`` command for sending "freeloader" barons back to the sites that they rule over +- `advtools`: ``advtools.fastcombat`` overlay (enabled by default) allows you to skip combat animations and the announcement "More" button by mashing the movement keys +- `gui/journal`: now working in adventure mode -- journal is per-adventurer, so if you unretire an adventurer, you get the same journal +- `emigration`: ``nobles`` command for sending freeloader barons back to the sites that they rule over +- `toggle-kbd-cursor`: support adventure mode (Alt-k keybinding now toggles Look mode) ## Fixes -- `position`: support for adv mode look cursor - `hfs-pit`: use correct wall types when making pits with walls +- `gui/liquids`: don't add liquids to wall tiles - `gui/liquids`: using the remove tool with magma selected will no longer create unexpected unpathable tiles -- `idle-crafting`: do not assign crafting jobs to nobles holding meetings (avoid dangling jobs) +- `idle-crafting`: do not assign crafting jobs to nobles holding meetings (avoids dangling jobs) - `rejuvenate`: update unit portrait and sprite when aging up babies and children - `rejuvenate`: recalculate labor assignments for unit when aging up babies and children (so they can start accepting jobs) -- `gui/liquids`: don't add liquids to wall tiles ## Misc Improvements - `hide-tutorials`: handle tutorial popups for adventure mode @@ -70,6 +65,11 @@ Template for new versions: - `prioritize`: when prioritizing jobs of a specified type, also output how many of those jobs were already prioritized before you ran the command - `prioritize`: don't include already-prioritized jobs in the output of ``prioritize -j`` - `gui/design`: only display vanilla dimensions tooltip if the DFHack dimensions tooltip is disabled +- `devel/query`: support adventure mode +- `devel/tree-info`: support adventure mode +- `hfs-pit`: support adventure mode +- `colonies`: support adventure mode +- `position`: report position of the adventure mode look cursor, if active ## Removed From 90431556c4c2924c206403232ea1c24491355d07 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 12 Mar 2025 10:09:19 -0700 Subject: [PATCH 481/811] update changelog --- changelog.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index ca3fc9ac3b..84df155092 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,18 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 51.07-r1 + ## New Tools - `devel/export-map`: export map tile data to a JSON file - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk @@ -71,8 +83,6 @@ Template for new versions: - `colonies`: support adventure mode - `position`: report position of the adventure mode look cursor, if active -## Removed - # 51.04-r1.1 ## Fixes From f9a65670b01f583783da4d2372b9beb1befcd53a Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 15 Mar 2025 14:47:55 +0100 Subject: [PATCH 482/811] spectate.lua: add activity --- gui/spectate.lua | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/gui/spectate.lua b/gui/spectate.lua index 3fb6ec49eb..3e7bbd09b8 100644 --- a/gui/spectate.lua +++ b/gui/spectate.lua @@ -46,7 +46,7 @@ end Spectate = defclass(Spectate, widgets.Window) Spectate.ATTRS { frame_title='Spectate', - frame={l=5, t=5, w=36, h=40}, + frame={l=5, t=5, w=36, h=41}, } local function create_toggle_button(frame, cfg_elem, hotkey, label, cfg_elem_key) @@ -242,9 +242,10 @@ function Spectate:init() }, create_row({t=26}, 'Job', 'J', 'job', colFollow, colHover), - create_row({t=27}, 'Name', 'N', 'name', colFollow, colHover), - create_row({t=28}, 'Stress', 'S', 'stress', colFollow, colHover), - create_stress_list({t=29}, colFollow, colHover), + create_row({t=27}, 'Activity', 'A', 'activity', colFollow, colHover), + create_row({t=28}, 'Name', 'N', 'name', colFollow, colHover), + create_row({t=29}, 'Stress', 'S', 'stress', colFollow, colHover), + create_stress_list({t=30}, colFollow, colHover), } end From 6b34c3bc3a5061a7922f06895b6466962893f80c Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 12:08:24 -0500 Subject: [PATCH 483/811] Create toolbar overlay for mass-remove --- mass-remove.lua | 115 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 mass-remove.lua diff --git a/mass-remove.lua b/mass-remove.lua new file mode 100644 index 0000000000..5150a36733 --- /dev/null +++ b/mass-remove.lua @@ -0,0 +1,115 @@ +--@ module = true + +local gui = require('gui') +local overlay = require('plugins.overlay') +local widgets = require('gui.widgets') + +local toolbar_textures = (dfhack.textures.loadTileset('hack/data/art/mass_remove_toolbar.png', 8,12)) + +local BASELINE_OFFSET = 42 + +local function get_l_offset(parent_rect) + local w = parent_rect.width + if w <= 177 then + return BASELINE_OFFSET + w - 114 + end + return BASELINE_OFFSET + (w+1)//2 - 26 +end + +function launch_mass_remove() + dfhack.run_command('gui/mass-remove') +end + + +-- -------------------------------- +-- MassRemoveToolbarOverlay +-- + +MassRemoveToolbarOverlay = defclass(MassRemoveToolbarOverlay, overlay.OverlayWidget) +MassRemoveToolbarOverlay.ATTRS{ + desc='Adds widgets to the erase interface to open the mass removal tool', + default_pos={x=BASELINE_OFFSET, y=-4}, + default_enabled=true, + viewscreens={ + 'dwarfmode/Designate/ERASE' + }, + frame={w=26, h=11}, +} + +function MassRemoveToolbarOverlay:init() + local button_chars = { + {218, 196, 196, 191}, + {179, 'M', 'R', 179}, + {192, 196, 196, 217}, + } + + self:addviews{ + widgets.Panel{ + frame={t=0, r=0, w=26, h=7}, + frame_style=gui.FRAME_PANEL, + frame_background=gui.CLEAR_PEN, + frame_inset={l=1, r=1}, + visible=function() return not not self.subviews.icon:getMousePos() end, + subviews={ + widgets.Label{ + text={ + 'Open mass removal\ninterface.\n', + NEWLINE, + {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_CTRL_M'}, + }, + }, + }, + }, + widgets.Panel{ + view_id='icon', + frame={b=0, r=22, w=4, h=3}, + subviews={ + widgets.Label{ + text=widgets.makeButtonLabelText{ + chars=button_chars, + pens=COLOR_GRAY, + tileset=toolbar_textures, + tileset_offset=1, + tileset_stride=8, + }, + on_click=launch_mass_remove, + visible=function () return not self.subviews.icon:getMousePos() end, + }, + widgets.Label{ + text=widgets.makeButtonLabelText{ + chars=button_chars, + pens={ + {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, + {COLOR_WHITE, COLOR_GRAY, COLOR_GRAY, COLOR_WHITE}, + {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, + }, + tileset=toolbar_textures, + tileset_offset=5, + tileset_stride=8, + }, + on_click=launch_mass_remove, + visible=function() return not not self.subviews.icon:getMousePos() end, + }, + }, + }, + } +end + +function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) + self.frame.w = get_l_offset(parent_rect) - BASELINE_OFFSET + 18 + print(self.frame.w) +end + +function MassRemoveToolbarOverlay:onInput(keys) + if keys.CUSTOM_CTRL_M then + launch_mass_remove() + return true + end + return MassRemoveToolbarOverlay.super.onInput(self, keys) +end + +OVERLAY_WIDGETS = {massremovetoolbar=MassRemoveToolbarOverlay} + +if dfhack_flags.module then + return +end \ No newline at end of file From c5bfc79d9fc93354786d02e2be672689d4612efd Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 12:12:21 -0500 Subject: [PATCH 484/811] fix EOF --- mass-remove.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mass-remove.lua b/mass-remove.lua index 5150a36733..62ffd95953 100644 --- a/mass-remove.lua +++ b/mass-remove.lua @@ -112,4 +112,4 @@ OVERLAY_WIDGETS = {massremovetoolbar=MassRemoveToolbarOverlay} if dfhack_flags.module then return -end \ No newline at end of file +end From a21dc0b61475c4d6589d172d638df653f6227403 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 12:31:03 -0500 Subject: [PATCH 485/811] remove dbug code that snuck in --- mass-remove.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/mass-remove.lua b/mass-remove.lua index 62ffd95953..eb5b0b8ce0 100644 --- a/mass-remove.lua +++ b/mass-remove.lua @@ -97,7 +97,6 @@ end function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) self.frame.w = get_l_offset(parent_rect) - BASELINE_OFFSET + 18 - print(self.frame.w) end function MassRemoveToolbarOverlay:onInput(keys) From 7bf28144c0f5ebecec04925d4c8ea59e132b6a98 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 14:27:09 -0500 Subject: [PATCH 486/811] Fix icon to not move when window resizes --- mass-remove.lua | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/mass-remove.lua b/mass-remove.lua index eb5b0b8ce0..f33f3361ff 100644 --- a/mass-remove.lua +++ b/mass-remove.lua @@ -10,10 +10,7 @@ local BASELINE_OFFSET = 42 local function get_l_offset(parent_rect) local w = parent_rect.width - if w <= 177 then - return BASELINE_OFFSET + w - 114 - end - return BASELINE_OFFSET + (w+1)//2 - 26 + return BASELINE_OFFSET + (w+1)//2 - 34 end function launch_mass_remove() From c7341ddacc99262d6f9887a37468f5bd9e830059 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 14:31:04 -0500 Subject: [PATCH 487/811] change the eraser menu hotkey to use just `m` --- mass-remove.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mass-remove.lua b/mass-remove.lua index f33f3361ff..a7b9879e83 100644 --- a/mass-remove.lua +++ b/mass-remove.lua @@ -52,7 +52,7 @@ function MassRemoveToolbarOverlay:init() text={ 'Open mass removal\ninterface.\n', NEWLINE, - {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_CTRL_M'}, + {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_M'}, }, }, }, @@ -97,7 +97,7 @@ function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) end function MassRemoveToolbarOverlay:onInput(keys) - if keys.CUSTOM_CTRL_M then + if keys.CUSTOM_M then launch_mass_remove() return true end From aafb0d1fc4fd25e718de6df6d3666deaa38ae0ce Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 14:31:34 -0500 Subject: [PATCH 488/811] close the eraser menu when we open mass remove so the ui is less cluttered --- mass-remove.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/mass-remove.lua b/mass-remove.lua index a7b9879e83..0050ddab03 100644 --- a/mass-remove.lua +++ b/mass-remove.lua @@ -14,6 +14,8 @@ local function get_l_offset(parent_rect) end function launch_mass_remove() + local vs = dfhack.gui.getDFViewscreen(true) + gui.simulateInput(vs,'LEAVESCREEN') dfhack.run_command('gui/mass-remove') end From 48d896bb93231611b3b8fd23f36d4737694ffea3 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Wed, 19 Mar 2025 23:09:16 -0500 Subject: [PATCH 489/811] Clean up clean up the looks of the toolt --- mass-remove.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mass-remove.lua b/mass-remove.lua index 0050ddab03..1c51887492 100644 --- a/mass-remove.lua +++ b/mass-remove.lua @@ -44,7 +44,7 @@ function MassRemoveToolbarOverlay:init() self:addviews{ widgets.Panel{ - frame={t=0, r=0, w=26, h=7}, + frame={t=0, r=0, w=26, h=6}, frame_style=gui.FRAME_PANEL, frame_background=gui.CLEAR_PEN, frame_inset={l=1, r=1}, @@ -52,7 +52,7 @@ function MassRemoveToolbarOverlay:init() subviews={ widgets.Label{ text={ - 'Open mass removal\ninterface.\n', + 'Open mass removal\ninterface.', NEWLINE, NEWLINE, {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_M'}, }, From 419cedff525488bcc9aa331940b04660b7777791 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 22 Mar 2025 18:48:22 -0500 Subject: [PATCH 490/811] Create mass-remove.rst --- docs/mass-remove.rst | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 docs/mass-remove.rst diff --git a/docs/mass-remove.rst b/docs/mass-remove.rst new file mode 100644 index 0000000000..6f17685edb --- /dev/null +++ b/docs/mass-remove.rst @@ -0,0 +1,6 @@ +mass-remove +=========== + +The mass-remove.massremovetoolbar overlay adds a button to the toolbar at the bottom of the +screen when eraser mode is active. It allows you to conveniently open the ``gui/mass-remove`` +interface. From f1b53c39cf9e384bf27428712bc9da822f7a273e Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 22 Mar 2025 18:56:09 -0500 Subject: [PATCH 491/811] Fix docs --- docs/gui/mass-remove.rst | 14 ++++++++++++++ docs/mass-remove.rst | 6 ------ 2 files changed, 14 insertions(+), 6 deletions(-) delete mode 100644 docs/mass-remove.rst diff --git a/docs/gui/mass-remove.rst b/docs/gui/mass-remove.rst index 6a3becb8f4..66e3b759fa 100644 --- a/docs/gui/mass-remove.rst +++ b/docs/gui/mass-remove.rst @@ -19,3 +19,17 @@ Usage :: gui/mass-remove + + +Overlay +------- + +This tool also provides one overlay that is managed by the `overlay` +framework. + +massremovetoolbar +~~~~~~~~~~~~~~~~~ + +The mass-remove.massremovetoolbar overlay adds a button to the toolbar at the bottom of the +screen when eraser mode is active. It allows you to conveniently open the ``gui/mass-remove`` +interface. diff --git a/docs/mass-remove.rst b/docs/mass-remove.rst deleted file mode 100644 index 6f17685edb..0000000000 --- a/docs/mass-remove.rst +++ /dev/null @@ -1,6 +0,0 @@ -mass-remove -=========== - -The mass-remove.massremovetoolbar overlay adds a button to the toolbar at the bottom of the -screen when eraser mode is active. It allows you to conveniently open the ``gui/mass-remove`` -interface. From b61d4cb881d814225e49cf857e91276eae9143ae Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 22 Mar 2025 19:04:12 -0500 Subject: [PATCH 492/811] merge into the real mass remove --- gui/mass-remove.lua | 108 ++++++++++++++++++++++++++++++++++++++++++ mass-remove.lua | 113 -------------------------------------------- 2 files changed, 108 insertions(+), 113 deletions(-) delete mode 100644 mass-remove.lua diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index f138c50d87..a141874f34 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -1,13 +1,31 @@ -- building/construction mass removal/suspension tool +--@ module = true + +local toolbar_textures = (dfhack.textures.loadTileset('hack/data/art/mass_remove_toolbar.png', 8,12)) + +local BASELINE_OFFSET = 42 + local gui = require('gui') local guidm = require('gui.dwarfmode') local utils = require('utils') local widgets = require('gui.widgets') +local overlay = require('plugins.overlay') local function noop() end +local function get_l_offset(parent_rect) + local w = parent_rect.width + return BASELINE_OFFSET + (w+1)//2 - 34 +end + +function launch_mass_remove() + local vs = dfhack.gui.getDFViewscreen(true) + gui.simulateInput(vs,'LEAVESCREEN') + dfhack.run_command('gui/mass-remove') +end + local function get_first_job(bld) if not bld then return end if #bld.jobs ~= 1 then return end @@ -383,6 +401,96 @@ function MassRemoveScreen:onDismiss() view = nil end + +-- -------------------------------- +-- MassRemoveToolbarOverlay +-- + +MassRemoveToolbarOverlay = defclass(MassRemoveToolbarOverlay, overlay.OverlayWidget) +MassRemoveToolbarOverlay.ATTRS{ + desc='Adds widgets to the erase interface to open the mass removal tool', + default_pos={x=BASELINE_OFFSET, y=-4}, + default_enabled=true, + viewscreens={ + 'dwarfmode/Designate/ERASE' + }, + frame={w=26, h=11}, +} + +function MassRemoveToolbarOverlay:init() + local button_chars = { + {218, 196, 196, 191}, + {179, 'M', 'R', 179}, + {192, 196, 196, 217}, + } + + self:addviews{ + widgets.Panel{ + frame={t=0, r=0, w=26, h=6}, + frame_style=gui.FRAME_PANEL, + frame_background=gui.CLEAR_PEN, + frame_inset={l=1, r=1}, + visible=function() return not not self.subviews.icon:getMousePos() end, + subviews={ + widgets.Label{ + text={ + 'Open mass removal\ninterface.', NEWLINE, + NEWLINE, + {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_M'}, + }, + }, + }, + }, + widgets.Panel{ + view_id='icon', + frame={b=0, r=22, w=4, h=3}, + subviews={ + widgets.Label{ + text=widgets.makeButtonLabelText{ + chars=button_chars, + pens=COLOR_GRAY, + tileset=toolbar_textures, + tileset_offset=1, + tileset_stride=8, + }, + on_click=launch_mass_remove, + visible=function () return not self.subviews.icon:getMousePos() end, + }, + widgets.Label{ + text=widgets.makeButtonLabelText{ + chars=button_chars, + pens={ + {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, + {COLOR_WHITE, COLOR_GRAY, COLOR_GRAY, COLOR_WHITE}, + {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, + }, + tileset=toolbar_textures, + tileset_offset=5, + tileset_stride=8, + }, + on_click=launch_mass_remove, + visible=function() return not not self.subviews.icon:getMousePos() end, + }, + }, + }, + } +end + +function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) + self.frame.w = get_l_offset(parent_rect) - BASELINE_OFFSET + 18 +end + +function MassRemoveToolbarOverlay:onInput(keys) + if keys.CUSTOM_M then + launch_mass_remove() + return true + end + return MassRemoveToolbarOverlay.super.onInput(self, keys) +end + +OVERLAY_WIDGETS = {massremovetoolbar=MassRemoveToolbarOverlay} + + if dfhack_flags.module then return end diff --git a/mass-remove.lua b/mass-remove.lua deleted file mode 100644 index 1c51887492..0000000000 --- a/mass-remove.lua +++ /dev/null @@ -1,113 +0,0 @@ ---@ module = true - -local gui = require('gui') -local overlay = require('plugins.overlay') -local widgets = require('gui.widgets') - -local toolbar_textures = (dfhack.textures.loadTileset('hack/data/art/mass_remove_toolbar.png', 8,12)) - -local BASELINE_OFFSET = 42 - -local function get_l_offset(parent_rect) - local w = parent_rect.width - return BASELINE_OFFSET + (w+1)//2 - 34 -end - -function launch_mass_remove() - local vs = dfhack.gui.getDFViewscreen(true) - gui.simulateInput(vs,'LEAVESCREEN') - dfhack.run_command('gui/mass-remove') -end - - --- -------------------------------- --- MassRemoveToolbarOverlay --- - -MassRemoveToolbarOverlay = defclass(MassRemoveToolbarOverlay, overlay.OverlayWidget) -MassRemoveToolbarOverlay.ATTRS{ - desc='Adds widgets to the erase interface to open the mass removal tool', - default_pos={x=BASELINE_OFFSET, y=-4}, - default_enabled=true, - viewscreens={ - 'dwarfmode/Designate/ERASE' - }, - frame={w=26, h=11}, -} - -function MassRemoveToolbarOverlay:init() - local button_chars = { - {218, 196, 196, 191}, - {179, 'M', 'R', 179}, - {192, 196, 196, 217}, - } - - self:addviews{ - widgets.Panel{ - frame={t=0, r=0, w=26, h=6}, - frame_style=gui.FRAME_PANEL, - frame_background=gui.CLEAR_PEN, - frame_inset={l=1, r=1}, - visible=function() return not not self.subviews.icon:getMousePos() end, - subviews={ - widgets.Label{ - text={ - 'Open mass removal\ninterface.', NEWLINE, - NEWLINE, - {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_M'}, - }, - }, - }, - }, - widgets.Panel{ - view_id='icon', - frame={b=0, r=22, w=4, h=3}, - subviews={ - widgets.Label{ - text=widgets.makeButtonLabelText{ - chars=button_chars, - pens=COLOR_GRAY, - tileset=toolbar_textures, - tileset_offset=1, - tileset_stride=8, - }, - on_click=launch_mass_remove, - visible=function () return not self.subviews.icon:getMousePos() end, - }, - widgets.Label{ - text=widgets.makeButtonLabelText{ - chars=button_chars, - pens={ - {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, - {COLOR_WHITE, COLOR_GRAY, COLOR_GRAY, COLOR_WHITE}, - {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, - }, - tileset=toolbar_textures, - tileset_offset=5, - tileset_stride=8, - }, - on_click=launch_mass_remove, - visible=function() return not not self.subviews.icon:getMousePos() end, - }, - }, - }, - } -end - -function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) - self.frame.w = get_l_offset(parent_rect) - BASELINE_OFFSET + 18 -end - -function MassRemoveToolbarOverlay:onInput(keys) - if keys.CUSTOM_M then - launch_mass_remove() - return true - end - return MassRemoveToolbarOverlay.super.onInput(self, keys) -end - -OVERLAY_WIDGETS = {massremovetoolbar=MassRemoveToolbarOverlay} - -if dfhack_flags.module then - return -end From 74237a24ef616b111c85066090af20c994334181 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 22 Mar 2025 19:12:52 -0500 Subject: [PATCH 493/811] clean up --- gui/mass-remove.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index a141874f34..df6ad4ed80 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -23,7 +23,7 @@ end function launch_mass_remove() local vs = dfhack.gui.getDFViewscreen(true) gui.simulateInput(vs,'LEAVESCREEN') - dfhack.run_command('gui/mass-remove') + MassRemoveScreen{}:show() end local function get_first_job(bld) From 8e21b02c0737abb8591d41c2b492d01b5118a309 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 22 Mar 2025 19:21:03 -0500 Subject: [PATCH 494/811] Apply suggestions from code review Co-authored-by: Myk --- docs/gui/mass-remove.rst | 2 +- gui/mass-remove.lua | 11 +++++------ 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/gui/mass-remove.rst b/docs/gui/mass-remove.rst index 66e3b759fa..01776ce7c9 100644 --- a/docs/gui/mass-remove.rst +++ b/docs/gui/mass-remove.rst @@ -30,6 +30,6 @@ framework. massremovetoolbar ~~~~~~~~~~~~~~~~~ -The mass-remove.massremovetoolbar overlay adds a button to the toolbar at the bottom of the +The ``mass-remove.massremovetoolbar`` overlay adds a button to the toolbar at the bottom of the screen when eraser mode is active. It allows you to conveniently open the ``gui/mass-remove`` interface. diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index df6ad4ed80..512324b0f9 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -2,7 +2,7 @@ --@ module = true -local toolbar_textures = (dfhack.textures.loadTileset('hack/data/art/mass_remove_toolbar.png', 8,12)) +local toolbar_textures = dfhack.textures.loadTileset('hack/data/art/mass_remove_toolbar.png', 8, 12) local BASELINE_OFFSET = 42 @@ -408,12 +408,10 @@ end MassRemoveToolbarOverlay = defclass(MassRemoveToolbarOverlay, overlay.OverlayWidget) MassRemoveToolbarOverlay.ATTRS{ - desc='Adds widgets to the erase interface to open the mass removal tool', + desc='Adds a button to the erase toolbar to open the mass removal tool', default_pos={x=BASELINE_OFFSET, y=-4}, default_enabled=true, - viewscreens={ - 'dwarfmode/Designate/ERASE' - }, + viewscreens='dwarfmode/Designate/ERASE', frame={w=26, h=11}, } @@ -434,7 +432,8 @@ function MassRemoveToolbarOverlay:init() subviews={ widgets.Label{ text={ - 'Open mass removal\ninterface.', NEWLINE, + 'Open mass removal', NEWLINE, + 'interface.', NEWLINE, NEWLINE, {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_M'}, }, From db1ce8faf42720a90469979d046e9044e603fc8e Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 22 Mar 2025 19:21:21 -0500 Subject: [PATCH 495/811] Apply suggestions from code review Co-authored-by: Myk --- docs/gui/mass-remove.rst | 2 -- 1 file changed, 2 deletions(-) diff --git a/docs/gui/mass-remove.rst b/docs/gui/mass-remove.rst index 01776ce7c9..ae3edc2879 100644 --- a/docs/gui/mass-remove.rst +++ b/docs/gui/mass-remove.rst @@ -19,8 +19,6 @@ Usage :: gui/mass-remove - - Overlay ------- From 26f05180bf3371eedc6a0b3c2c1d667968c49247 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 22 Mar 2025 19:26:57 -0500 Subject: [PATCH 496/811] Update from code review --- gui/mass-remove.lua | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 512324b0f9..c63b5bbf9c 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -15,11 +15,6 @@ local overlay = require('plugins.overlay') local function noop() end -local function get_l_offset(parent_rect) - local w = parent_rect.width - return BASELINE_OFFSET + (w+1)//2 - 34 -end - function launch_mass_remove() local vs = dfhack.gui.getDFViewscreen(true) gui.simulateInput(vs,'LEAVESCREEN') @@ -428,7 +423,7 @@ function MassRemoveToolbarOverlay:init() frame_style=gui.FRAME_PANEL, frame_background=gui.CLEAR_PEN, frame_inset={l=1, r=1}, - visible=function() return not not self.subviews.icon:getMousePos() end, + visible=function() return self.subviews.icon:getMousePos() end, subviews={ widgets.Label{ text={ @@ -468,7 +463,7 @@ function MassRemoveToolbarOverlay:init() tileset_stride=8, }, on_click=launch_mass_remove, - visible=function() return not not self.subviews.icon:getMousePos() end, + visible=function() return self.subviews.icon:getMousePos() end, }, }, }, @@ -476,7 +471,7 @@ function MassRemoveToolbarOverlay:init() end function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) - self.frame.w = get_l_offset(parent_rect) - BASELINE_OFFSET + 18 + self.frame.w = (parent_rect.width+1)//2 - 16 end function MassRemoveToolbarOverlay:onInput(keys) From 1c86714fc04d2a0e9617f824f690760ccb84da4c Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 22 Mar 2025 19:47:21 -0500 Subject: [PATCH 497/811] remove Baseline offset --- gui/mass-remove.lua | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index c63b5bbf9c..1276b12dae 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -4,8 +4,6 @@ local toolbar_textures = dfhack.textures.loadTileset('hack/data/art/mass_remove_toolbar.png', 8, 12) -local BASELINE_OFFSET = 42 - local gui = require('gui') local guidm = require('gui.dwarfmode') local utils = require('utils') @@ -404,7 +402,7 @@ end MassRemoveToolbarOverlay = defclass(MassRemoveToolbarOverlay, overlay.OverlayWidget) MassRemoveToolbarOverlay.ATTRS{ desc='Adds a button to the erase toolbar to open the mass removal tool', - default_pos={x=BASELINE_OFFSET, y=-4}, + default_pos={x=42, y=-4}, default_enabled=true, viewscreens='dwarfmode/Designate/ERASE', frame={w=26, h=11}, From 040da8be760f6096aa17a1e5bd3830101f6c4b09 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 22 Mar 2025 19:50:05 -0500 Subject: [PATCH 498/811] Update mass-remove.lua --- gui/mass-remove.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 1276b12dae..fc4546b467 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -16,7 +16,7 @@ end function launch_mass_remove() local vs = dfhack.gui.getDFViewscreen(true) gui.simulateInput(vs,'LEAVESCREEN') - MassRemoveScreen{}:show() + dfhack.run_script('gui/mass-remove') end local function get_first_job(bld) From 3da987da452e09ee6e16c2c0d6be73e3f5d51796 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 22 Mar 2025 19:55:38 -0500 Subject: [PATCH 499/811] fix docs error --- docs/gui/mass-remove.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/gui/mass-remove.rst b/docs/gui/mass-remove.rst index ae3edc2879..1a8ea4b549 100644 --- a/docs/gui/mass-remove.rst +++ b/docs/gui/mass-remove.rst @@ -19,6 +19,7 @@ Usage :: gui/mass-remove + Overlay ------- From c14f328cd45640a2e44378ebd37b00e2b06b19c5 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 22 Mar 2025 20:01:59 -0500 Subject: [PATCH 500/811] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index c4cb15b52c..56aa6b5845 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,7 +29,7 @@ Template for new versions: ## New Tools ## New Features - +- `gui/mass-remove`: added a button to the bottom toolbar when eraser mode is active for launching `gui/mass-remove` - `idle-crafting`: default to only considering happy and ecstatic units for the highest need threshold ## Fixes From 42160636e834eb44f16497a5acc42cf573118d37 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Mar 2025 18:09:49 -0700 Subject: [PATCH 501/811] fix widget name --- docs/gui/mass-remove.rst | 10 +++++----- gui/mass-remove.lua | 3 +-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/docs/gui/mass-remove.rst b/docs/gui/mass-remove.rst index 1a8ea4b549..3829d3b2c2 100644 --- a/docs/gui/mass-remove.rst +++ b/docs/gui/mass-remove.rst @@ -26,9 +26,9 @@ Overlay This tool also provides one overlay that is managed by the `overlay` framework. -massremovetoolbar -~~~~~~~~~~~~~~~~~ +gui/mass-remove.toolbar +~~~~~~~~~~~~~~~~~~~~~~~ -The ``mass-remove.massremovetoolbar`` overlay adds a button to the toolbar at the bottom of the -screen when eraser mode is active. It allows you to conveniently open the ``gui/mass-remove`` -interface. +The ``gui/mass-remove.toolbar`` overlay adds a button to the toolbar at the +bottom of the screen when eraser mode is active. It allows you to conveniently +open the ``gui/mass-remove`` interface. diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index fc4546b467..37a8527efd 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -480,8 +480,7 @@ function MassRemoveToolbarOverlay:onInput(keys) return MassRemoveToolbarOverlay.super.onInput(self, keys) end -OVERLAY_WIDGETS = {massremovetoolbar=MassRemoveToolbarOverlay} - +OVERLAY_WIDGETS = {toolbar=MassRemoveToolbarOverlay} if dfhack_flags.module then return From 2345b2449efbd4e23ab356b3492de4c76bd26df6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Mar 2025 18:46:25 -0700 Subject: [PATCH 502/811] add missing period --- gui/mass-remove.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 37a8527efd..75a62009d2 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -401,7 +401,7 @@ end MassRemoveToolbarOverlay = defclass(MassRemoveToolbarOverlay, overlay.OverlayWidget) MassRemoveToolbarOverlay.ATTRS{ - desc='Adds a button to the erase toolbar to open the mass removal tool', + desc='Adds a button to the erase toolbar to open the mass removal tool.', default_pos={x=42, y=-4}, default_enabled=true, viewscreens='dwarfmode/Designate/ERASE', From 1d67a72ae72bbb7578b73a8c8a8fdae2cc671103 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Mar 2025 19:09:03 -0700 Subject: [PATCH 503/811] reduce console spam --- idle-crafting.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/idle-crafting.lua b/idle-crafting.lua index 33518f3522..6744f02fde 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -533,7 +533,7 @@ local function main_loop() for idx, threshold in ipairs(thresholds) do if ignore_happy and is_happy and threshold < max_threshold then -- ignore happy and ecstatic units for any threshold but the highest - print(dfhack.df2console(("idle-crafting: skipping happy unit %s"):format(dfhack.units.getReadableName(unit)))) + -- print(dfhack.df2console(("idle-crafting: skipping happy unit %s"):format(dfhack.units.getReadableName(unit)))) goto next_unit end From 9fe2a467fe5c2c51a74db02c6a317453904e095f Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 22 Mar 2025 21:18:43 -0500 Subject: [PATCH 504/811] Add ui button for `gui/sitemap` (#1423) * Make sitemap toolbar button overlay Co-authored-by: Myk --- changelog.txt | 1 + docs/gui/sitemap.rst | 13 +++++++ gui/sitemap.lua | 88 ++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 102 insertions(+) diff --git a/changelog.txt b/changelog.txt index 56aa6b5845..b24a1ff32e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features - `gui/mass-remove`: added a button to the bottom toolbar when eraser mode is active for launching `gui/mass-remove` - `idle-crafting`: default to only considering happy and ecstatic units for the highest need threshold +- `gui/sitemap`: added a button to the toolbar at the bottom left corner of the screen with the other menu buttons for launching `gui/sitemap` ## Fixes diff --git a/docs/gui/sitemap.rst b/docs/gui/sitemap.rst index ed1d89521d..493f3ba5f1 100644 --- a/docs/gui/sitemap.rst +++ b/docs/gui/sitemap.rst @@ -24,3 +24,16 @@ Usage :: gui/sitemap + +Overlay +------- + +This tool also provides one overlay that is managed by the `overlay` +framework. + +gui/sitemap.toolbar +~~~~~~~~~~~~~~~~~~~ + +The ``gui/sitemap.toolbar`` overlay adds a button to the toolbar at the bottom left corner of the +screen with the other menu buttons. It allows you to conveniently open the ``gui/sitemap`` +interface. diff --git a/gui/sitemap.lua b/gui/sitemap.lua index 671212f919..ffc4082c54 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -3,6 +3,13 @@ local gui = require('gui') local utils = require('utils') local widgets = require('gui.widgets') +local overlay = require('plugins.overlay') + +local toolbar_textures = dfhack.textures.loadTileset('hack/data/art/sitemap_toolbar.png', 8, 12) + +function launch_sitemap() + dfhack.run_script('gui/sitemap') +end -- -- Sitemap @@ -353,6 +360,87 @@ function SitemapScreen:onDismiss() view = nil end + +-- -------------------------------- +-- SitemapToolbarOverlay +-- + +SitemapToolbarOverlay = defclass(SitemapToolbarOverlay, overlay.OverlayWidget) +SitemapToolbarOverlay.ATTRS{ + desc='Adds a button to the toolbar at the bottom left corner of the screen for launching gui/sitemap.', + default_pos={x=35, y=-1}, + default_enabled=true, + viewscreens='dwarfmode', + frame={w=28, h=9}, +} + +function SitemapToolbarOverlay:init() + local button_chars = { + {218, 196, 196, 191}, + {179, '-', 'O', 179}, + {192, 196, 196, 217}, + } + + self:addviews{ + widgets.Panel{ + frame={t=0, l=0, w=26, h=5}, + frame_style=gui.FRAME_PANEL, + frame_background=gui.CLEAR_PEN, + frame_inset={l=1, r=1}, + visible=function() return self.subviews.icon:getMousePos() end, + subviews={ + widgets.Label{ + text={ + 'Open the general search', NEWLINE, + 'interface.', NEWLINE, + NEWLINE, + {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_CTRL_G'}, + }, + }, + }, + }, + widgets.Panel{ + view_id='icon', + frame={b=0, l=0, w=4, h=3}, + subviews={ + widgets.Label{ + text=widgets.makeButtonLabelText{ + chars=button_chars, + pens=COLOR_GRAY, + tileset=toolbar_textures, + tileset_offset=1, + tileset_stride=8, + }, + on_click=launch_sitemap, + visible=function () return not self.subviews.icon:getMousePos() end, + }, + widgets.Label{ + text=widgets.makeButtonLabelText{ + chars=button_chars, + pens={ + {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, + {COLOR_WHITE, COLOR_GRAY, COLOR_GRAY, COLOR_WHITE}, + {COLOR_WHITE, COLOR_WHITE, COLOR_WHITE, COLOR_WHITE}, + }, + tileset=toolbar_textures, + tileset_offset=5, + tileset_stride=8, + }, + on_click=launch_sitemap, + visible=function() return not not self.subviews.icon:getMousePos() end, + }, + }, + }, + } +end + +function SitemapToolbarOverlay:onInput(keys) + return SitemapToolbarOverlay.super.onInput(self, keys) +end + +OVERLAY_WIDGETS = {toolbar=SitemapToolbarOverlay} + + if dfhack_flags.module then return end From 57ee5f4e18f57e489f6e439a5c7654a97d71ccde Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Mar 2025 19:20:02 -0700 Subject: [PATCH 505/811] changelog editing --- changelog.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index b24a1ff32e..cd1830ff14 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,12 +29,11 @@ Template for new versions: ## New Tools ## New Features -- `gui/mass-remove`: added a button to the bottom toolbar when eraser mode is active for launching `gui/mass-remove` +- `gui/mass-remove`: add a button to the bottom toolbar when eraser mode is active for launching `gui/mass-remove` - `idle-crafting`: default to only considering happy and ecstatic units for the highest need threshold -- `gui/sitemap`: added a button to the toolbar at the bottom left corner of the screen with the other menu buttons for launching `gui/sitemap` +- `gui/sitemap`: add a button to the toolbar at the bottom left corner of the screen for launching `gui/sitemap` ## Fixes - - `idle-crafting`: check that units still have crafting needs before creating a job for them ## Misc Improvements From ccdc48f2dd4d1a777dc9d1002c707202ffdffff3 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 22 Mar 2025 20:55:19 -0700 Subject: [PATCH 506/811] fix dimensions of gui/sitemap overlay tooltip --- gui/sitemap.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/sitemap.lua b/gui/sitemap.lua index ffc4082c54..b3fbd796df 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -371,7 +371,7 @@ SitemapToolbarOverlay.ATTRS{ default_pos={x=35, y=-1}, default_enabled=true, viewscreens='dwarfmode', - frame={w=28, h=9}, + frame={w=28, h=10}, } function SitemapToolbarOverlay:init() @@ -383,7 +383,7 @@ function SitemapToolbarOverlay:init() self:addviews{ widgets.Panel{ - frame={t=0, l=0, w=26, h=5}, + frame={t=0, l=0, w=27, h=6}, frame_style=gui.FRAME_PANEL, frame_background=gui.CLEAR_PEN, frame_inset={l=1, r=1}, From 74ac117b19a6fcae99e92d3fece147687acef3ad Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 31 Mar 2025 18:12:44 -0700 Subject: [PATCH 507/811] don't pass pause events through the ui can happen if keys are mashed in just the right way --- changelog.txt | 1 + gui/journal.lua | 1 + 2 files changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index cd1830ff14..ea51b4d222 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: ## Fixes - `idle-crafting`: check that units still have crafting needs before creating a job for them +- `gui/journal`: prevent pause/unpause events from leaking through the UI when keys are mashed ## Misc Improvements diff --git a/gui/journal.lua b/gui/journal.lua index 5010933392..4faacb2183 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -268,6 +268,7 @@ end JournalScreen = defclass(JournalScreen, gui.ZScreen) JournalScreen.ATTRS { focus_path='journal', + pass_pause=false, context_mode=DEFAULT_NIL, save_layout=true, save_prefix='' From 5a9f303a478e5ec3fdfe1440460b95b4d4641e6e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 31 Mar 2025 18:37:33 -0700 Subject: [PATCH 508/811] fix tooltip offset for toolbar button --- gui/mass-remove.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 75a62009d2..4da45dc89f 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -405,7 +405,7 @@ MassRemoveToolbarOverlay.ATTRS{ default_pos={x=42, y=-4}, default_enabled=true, viewscreens='dwarfmode/Designate/ERASE', - frame={w=26, h=11}, + frame={w=26, h=10}, } function MassRemoveToolbarOverlay:init() From 20d201a8ffe5df52563a37fdde88986973faca4e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 31 Mar 2025 18:51:11 -0700 Subject: [PATCH 509/811] update wording on tooltip --- gui/sitemap.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/sitemap.lua b/gui/sitemap.lua index b3fbd796df..38c4504085 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -392,7 +392,7 @@ function SitemapToolbarOverlay:init() widgets.Label{ text={ 'Open the general search', NEWLINE, - 'interface.', NEWLINE, + 'and zoom interface.', NEWLINE, NEWLINE, {text='Hotkey: ', pen=COLOR_GRAY}, {key='CUSTOM_CTRL_G'}, }, From 31e31bf18728c9913deb234ef47a6f11a7208a1d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 31 Mar 2025 19:10:25 -0700 Subject: [PATCH 510/811] fix offset of toolbar button so it's consistent on large and small windows --- gui/mass-remove.lua | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index 4da45dc89f..ca3cb8aab6 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -469,7 +469,12 @@ function MassRemoveToolbarOverlay:init() end function MassRemoveToolbarOverlay:preUpdateLayout(parent_rect) - self.frame.w = (parent_rect.width+1)//2 - 16 + local w = parent_rect.width + if w <= 130 then + self.frame.w = 50 + else + self.frame.w = (parent_rect.width+1)//2 - 15 + end end function MassRemoveToolbarOverlay:onInput(keys) From 3a919bc098a08e0e1c259f82d03ecf268bd10a41 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 31 Mar 2025 19:15:03 -0700 Subject: [PATCH 511/811] move sitemap toolbar button over one pixel --- gui/sitemap.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/sitemap.lua b/gui/sitemap.lua index 38c4504085..576966f44c 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -368,7 +368,7 @@ end SitemapToolbarOverlay = defclass(SitemapToolbarOverlay, overlay.OverlayWidget) SitemapToolbarOverlay.ATTRS{ desc='Adds a button to the toolbar at the bottom left corner of the screen for launching gui/sitemap.', - default_pos={x=35, y=-1}, + default_pos={x=34, y=-1}, default_enabled=true, viewscreens='dwarfmode', frame={w=28, h=10}, From d8912409bb3c7398a2859aa0c0092d792c9211e7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 2 Apr 2025 08:57:29 -0700 Subject: [PATCH 512/811] update changelog --- changelog.txt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index ea51b4d222..10f3f37b29 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,16 @@ Template for new versions: ## New Tools +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 51.09-r1 + ## New Features - `gui/mass-remove`: add a button to the bottom toolbar when eraser mode is active for launching `gui/mass-remove` - `idle-crafting`: default to only considering happy and ecstatic units for the highest need threshold @@ -37,10 +47,6 @@ Template for new versions: - `idle-crafting`: check that units still have crafting needs before creating a job for them - `gui/journal`: prevent pause/unpause events from leaking through the UI when keys are mashed -## Misc Improvements - -## Removed - # 51.07-r1 ## New Tools From eec7afc49d931ed89fe6dcf54b303ad9df7885fc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 3 Apr 2025 15:58:34 -0700 Subject: [PATCH 513/811] remove unit from current conflict activities --- changelog.txt | 2 ++ docs/fix/loyaltycascade.rst | 4 +++- docs/makeown.rst | 3 ++- fix/loyaltycascade.lua | 5 +++-- makeown.lua | 31 ++++++++++++++++++++++++++++++- 5 files changed, 40 insertions(+), 5 deletions(-) diff --git a/changelog.txt b/changelog.txt index 10f3f37b29..dcde0e75a8 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,8 @@ Template for new versions: ## Fixes ## Misc Improvements +- `fix/loyaltycascade`: now also breaks up brawls and other intra-fort conflicts that *look* like loyalty cascades +- `makeown`: remove selected unit from any current conflicts so they don't just start attacking other citizens when you make them a citizen of your fort ## Removed diff --git a/docs/fix/loyaltycascade.rst b/docs/fix/loyaltycascade.rst index 39899aae3f..d4448f8c56 100644 --- a/docs/fix/loyaltycascade.rst +++ b/docs/fix/loyaltycascade.rst @@ -6,7 +6,9 @@ fix/loyaltycascade :tags: fort bugfix units This tool neutralizes loyalty cascades by fixing units who consider their own -civilization to be the enemy. +civilization to be the enemy. It will also halt all fighting on the map that +involves your citizens, though "real" enemies will re-engage in combat after a +short delay. Usage ----- diff --git a/docs/makeown.rst b/docs/makeown.rst index 7e2a53ba8a..136e8f5100 100644 --- a/docs/makeown.rst +++ b/docs/makeown.rst @@ -6,7 +6,8 @@ makeown :tags: fort armok units Select a unit in the UI and run this tool to converts that unit to be a fortress -citizen (if sentient). It also removes their foreign affiliation, if any. +citizen (if sentient). It also removes their foreign affiliation, if any, and +removes the unit from any current conflict they are engaged in. This tool also fixes :bug:`10921`, where you request workers from your holdings, but they come with the "Merchant" profession and are unable to diff --git a/fix/loyaltycascade.lua b/fix/loyaltycascade.lua index 768fbdfcf2..3409750cb4 100644 --- a/fix/loyaltycascade.lua +++ b/fix/loyaltycascade.lua @@ -1,4 +1,5 @@ -- Prevents a "loyalty cascade" (intra-fort civil war) when a citizen is killed. +-- Also breaks up brawls and other conflicts. local makeown = reqscript('makeown') @@ -76,7 +77,7 @@ local function fixUnit(unit) makeown.clear_enemy_status(unit) end - return false + return makeown.remove_from_conflict(unit) or fixed end local count = 0 @@ -87,7 +88,7 @@ for _, unit in pairs(dfhack.units.getCitizens()) do end if count > 0 then - print(('Fixed %s units from a loyalty cascade.'):format(count)) + print(('Fixed %s units with loyalty issues.'):format(count)) else print('No loyalty cascade found.') end diff --git a/makeown.lua b/makeown.lua index 244c83a93c..f7657f3250 100644 --- a/makeown.lua +++ b/makeown.lua @@ -81,6 +81,35 @@ function clear_enemy_status(unit) if status_cache.next_slot > status_slot then status_cache.next_slot = status_slot end + + return true +end + +function remove_from_conflict(unit) + -- Remove the unit from any conflict activity + -- They will prompty re-engage if there is an actual enemy around + local to_remove = {} + for act_idx,act_id in ipairs(unit.activities) do + local act = df.activity_entry.find(act_id) + if not act or act.type ~= df.activity_entry_type.Conflict then goto continue end + for _,ev in ipairs(act.events) do + if ev:getType() ~= df.activity_event_type.Conflict then goto next_ev end + for _,side in ipairs(ev.sides) do + utils.erase_sorted(side.histfig_ids, unit.hist_figure_id) + utils.erase_sorted(side.unit_ids, unit.id) + end + ::next_ev:: + end + table.insert(to_remove, 1, act_idx) + ::continue:: + end + + for _,act_idx in ipairs(to_remove) do + unit.activities:erase(act_idx) + end + + -- return whether we removed unit from any conflicts + return #to_remove > 0 end local prof_map = { @@ -162,7 +191,7 @@ local function fix_unit(unit) end clear_enemy_status(unit) - + remove_from_conflict(unit) cancel_hostile_jobs(unit.job.current_job) end From e7f1db0a30527f859d735d9da721b397abc0f6a7 Mon Sep 17 00:00:00 2001 From: Quietust Date: Fri, 4 Apr 2025 09:01:30 -0600 Subject: [PATCH 514/811] Fix logic for calculating petition age --- changelog.txt | 1 + list-agreements.lua | 16 +++++++--------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/changelog.txt b/changelog.txt index dcde0e75a8..e24d0b1017 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `list-agreements`: fix logic for determining age of active petitions ## Misc Improvements - `fix/loyaltycascade`: now also breaks up brawls and other intra-fort conflicts that *look* like loyalty cascades diff --git a/list-agreements.lua b/list-agreements.lua index 55c53a7860..887eaf1744 100644 --- a/list-agreements.lua +++ b/list-agreements.lua @@ -35,16 +35,14 @@ function get_petition_age(agr) local agr_year = agr.details[0].year local cur_year_tick = df.global.cur_year_tick local cur_year = df.global.cur_year - local del_year, del_year_tick - --delta, check to prevent off by 1 error, not validated - if cur_year_tick > agr_year_tick then - del_year = cur_year - agr_year - del_year_tick = cur_year_tick - agr_year_tick - else - del_year = cur_year - agr_year - 1 - del_year_tick = agr_year_tick - cur_year_tick + local del_year = cur_year - agr_year + local del_year_tick = cur_year_tick - agr_year_tick + if del_year_tick < 0 then + del_year = del_year - 1 + del_year_tick = del_year_tick + 403200 end - local julian_day = math.floor(del_year_tick / 1200) + 1 + -- Round up to the nearest day, since we don't do fractions + local julian_day = math.ceil(del_year_tick / 1200) local del_month = math.floor(julian_day / 28) local del_day = julian_day % 28 return {del_year,del_month,del_day} From 1f7663b57bdda6bb02dc2f950f3465df832b2fe6 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 4 Apr 2025 18:09:14 -0700 Subject: [PATCH 515/811] update changelog --- changelog.txt | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index e24d0b1017..213e521fd3 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,7 +31,8 @@ Template for new versions: ## New Features ## Fixes -- `list-agreements`: fix logic for determining age of active petitions +- `list-agreements`: fix date math when determining petition age +- `gui/petitions`: fix date math when determining petition age ## Misc Improvements - `fix/loyaltycascade`: now also breaks up brawls and other intra-fort conflicts that *look* like loyalty cascades From a530615ec6624d99599b1df6cbdf732d9cfa5f6e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Apr 2025 19:32:54 +0000 Subject: [PATCH 516/811] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.31.2 → 0.32.1](https://github.com/python-jsonschema/check-jsonschema/compare/0.31.2...0.32.1) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 14a734aa5a..f601d4c321 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.31.2 + rev: 0.32.1 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From 4cacf13427f3a7a69cdb3920a99041b4821f3fda Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 12 Apr 2025 10:09:00 -0500 Subject: [PATCH 517/811] Update deathcause.lua make it more api-like --- deathcause.lua | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index 953f36bd22..a735aee249 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -29,8 +29,7 @@ function displayDeathUnit(unit) str = str .. dfhack.units.getReadableName(unit) if not dfhack.units.isDead(unit) then - print(dfhack.df2console(str) .. " is not dead yet!") - return + return(dfhack.df2console(str) .. " is not dead yet!") end str = str .. (" %s"):format(getDeathStringFromCause(unit.counters.death_cause)) @@ -50,7 +49,7 @@ function displayDeathUnit(unit) end end - print(dfhack.df2console(str) .. '.') + return(dfhack.df2console(str) .. '.') end -- returns the item description if the item still exists; otherwise @@ -87,7 +86,7 @@ function displayDeathEventHistFigUnit(histfig_unit, event) end end - print(dfhack.df2console(str) .. '.') + return(dfhack.df2console(str) .. '.') end -- Returns the death event for the given histfig or nil if not found @@ -109,10 +108,10 @@ function displayDeathHistFig(histfig) end if not dfhack.units.isDead(histfig_unit) then - print(("%s is not dead yet!"):format(dfhack.df2console(dfhack.units.getReadableName(histfig_unit)))) + return(("%s is not dead yet!"):format(dfhack.df2console(dfhack.units.getReadableName(histfig_unit)))) else local death_event = getDeathEventForHistFig(histfig.id) - displayDeathEventHistFigUnit(histfig_unit, death_event) + return displayDeathEventHistFigUnit(histfig_unit, death_event) end end @@ -155,7 +154,7 @@ elseif hist_figure_id == -1 then if not selected_unit then qerror("Cause of death not available") end - displayDeathUnit(selected_unit) + print(displayDeathUnit(selected_unit)) else - displayDeathHistFig(df.historical_figure.find(hist_figure_id)) + print(displayDeathHistFig(df.historical_figure.find(hist_figure_id))) end From c4483b557f10c4c7c4a625badd4cb02cd572d1c3 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 12 Apr 2025 10:20:03 -0500 Subject: [PATCH 518/811] Update deathcause.lua Remove the console formatting Signed-off-by: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> --- deathcause.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index a735aee249..d07d62d866 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -29,7 +29,7 @@ function displayDeathUnit(unit) str = str .. dfhack.units.getReadableName(unit) if not dfhack.units.isDead(unit) then - return(dfhack.df2console(str) .. " is not dead yet!") + return(str .. " is not dead yet!") end str = str .. (" %s"):format(getDeathStringFromCause(unit.counters.death_cause)) @@ -49,7 +49,7 @@ function displayDeathUnit(unit) end end - return(dfhack.df2console(str) .. '.') + return(str .. '.') end -- returns the item description if the item still exists; otherwise @@ -86,7 +86,7 @@ function displayDeathEventHistFigUnit(histfig_unit, event) end end - return(dfhack.df2console(str) .. '.') + return( str .. '.') end -- Returns the death event for the given histfig or nil if not found @@ -108,7 +108,7 @@ function displayDeathHistFig(histfig) end if not dfhack.units.isDead(histfig_unit) then - return(("%s is not dead yet!"):format(dfhack.df2console(dfhack.units.getReadableName(histfig_unit)))) + return(("%s is not dead yet!"):format(dfhack.units.getReadableName(histfig_unit))) else local death_event = getDeathEventForHistFig(histfig.id) return displayDeathEventHistFigUnit(histfig_unit, death_event) From 2fb14b623074f9d4aeba73b2fb31feecc4e92fe8 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 12 Apr 2025 10:23:48 -0500 Subject: [PATCH 519/811] Update deathcause.lua Format to keep old usage working Signed-off-by: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> --- deathcause.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index d07d62d866..9ba2d73b34 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -154,7 +154,7 @@ elseif hist_figure_id == -1 then if not selected_unit then qerror("Cause of death not available") end - print(displayDeathUnit(selected_unit)) + print(dfhack.df2console(displayDeathUnit(selected_unit))) else - print(displayDeathHistFig(df.historical_figure.find(hist_figure_id))) + print(dfhack.df2console(displayDeathHistFig(df.historical_figure.find(hist_figure_id)))) end From e62e6d9a7ab43b0fc4ab430e28b6f16654a2b79a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 12 Apr 2025 14:33:31 -0700 Subject: [PATCH 520/811] fix overlay interactions in gui/notes unit tests --- gui/notes.lua | 3 +-- test/gui/notes.lua | 18 +++++++++--------- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/gui/notes.lua b/gui/notes.lua index 4d8eee7d4d..9ad4d8f822 100644 --- a/gui/notes.lua +++ b/gui/notes.lua @@ -361,8 +361,7 @@ function main() qerror('notes requires a fortress map to be loaded') end - view = view and view:raise() or NotesScreen{ - }:show() + view = view and view:raise() or NotesScreen{}:show() end if not dfhack_flags.module then diff --git a/test/gui/notes.lua b/test/gui/notes.lua index 9bb1c7ac76..815ba7bfaa 100644 --- a/test/gui/notes.lua +++ b/test/gui/notes.lua @@ -39,21 +39,21 @@ end local function arrange_gui_notes(options) options = options or {} + -- running tests removes all overlays because of IN_TEST reloading. + -- rescan so we can load the gui/notes widget for these tests + overlay.rescan() + arrange_notes(options.notes) gui_notes.main() - local gui_notes = gui_notes.view - gui_notes.enable_selector_blink = false + local view = gui_notes.view + view.enable_selector_blink = false - gui_notes:updateLayout() - gui_notes:onRender() - - -- for some reasons running tests remove all overlays, - -- but there are need for gui/notes tests - overlay.rescan() + view:updateLayout() + view:onRender() - return gui_notes, gui_notes.subviews.notes_window + return view, view.subviews.notes_window end local function cleanup(gui_notes) From cf4af78cc62114bdd478828d4573a0d05cabaf2b Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 12 Apr 2025 17:25:10 -0500 Subject: [PATCH 521/811] Update deathcause.lua final fixes --- deathcause.lua | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index 9ba2d73b34..d0a90dd6c9 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -1,4 +1,5 @@ -- show death cause of a creature +--@ module = true local DEATH_TYPES = reqscript('gui/unit-info-viewer').DEATH_TYPES @@ -29,7 +30,7 @@ function displayDeathUnit(unit) str = str .. dfhack.units.getReadableName(unit) if not dfhack.units.isDead(unit) then - return(str .. " is not dead yet!") + return str .. " is not dead yet!" end str = str .. (" %s"):format(getDeathStringFromCause(unit.counters.death_cause)) @@ -49,7 +50,7 @@ function displayDeathUnit(unit) end end - return(str .. '.') + return str .. '.' end -- returns the item description if the item still exists; otherwise @@ -86,7 +87,7 @@ function displayDeathEventHistFigUnit(histfig_unit, event) end end - return( str .. '.') + return str .. '.' end -- Returns the death event for the given histfig or nil if not found @@ -108,7 +109,7 @@ function displayDeathHistFig(histfig) end if not dfhack.units.isDead(histfig_unit) then - return(("%s is not dead yet!"):format(dfhack.units.getReadableName(histfig_unit))) + return ("%s is not dead yet!"):format(dfhack.units.getReadableName(histfig_unit)) else local death_event = getDeathEventForHistFig(histfig.id) return displayDeathEventHistFigUnit(histfig_unit, death_event) @@ -146,6 +147,10 @@ local function get_target() return selected_item.hist_figure_id, df.unit.find(selected_item.unit_id) end +if dfhack_flags.module then + return +end + local hist_figure_id, selected_unit = get_target() if not hist_figure_id then From 81d6b7571f04d31e3c48599ebe85276878e3210b Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 13 Apr 2025 08:24:14 -0700 Subject: [PATCH 522/811] actually read the argument from the commandline --- changelog.txt | 1 + gui/rename.lua | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/changelog.txt b/changelog.txt index 213e521fd3..55bfcf2f5c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes - `list-agreements`: fix date math when determining petition age - `gui/petitions`: fix date math when determining petition age +- `gui/rename`: fix commandline processing when manually specifying target ids ## Misc Improvements - `fix/loyaltycascade`: now also breaks up brawls and other intra-fort conflicts that *look* like loyalty cascades diff --git a/gui/rename.lua b/gui/rename.lua index 437e6ffc7f..397d651c4b 100644 --- a/gui/rename.lua +++ b/gui/rename.lua @@ -1022,14 +1022,14 @@ local function main(args) show_selector=true, } local positionals = argparse.processArgsGetopt(args, { - { 'a', 'artifact', handler=function(optarg) opts.item_id = argparse.nonnegativeInt(optarg, 'artifact') end }, - { 'e', 'entity', handler=function(optarg) opts.entity_id = argparse.nonnegativeInt(optarg, 'entity') end }, - { 'f', 'histfig', handler=function(optarg) opts.histfig_id = argparse.nonnegativeInt(optarg, 'histfig') end }, + { 'a', 'artifact', hasArg=true, handler=function(optarg) opts.item_id = argparse.nonnegativeInt(optarg, 'artifact') end }, + { 'e', 'entity', hasArg=true, handler=function(optarg) opts.entity_id = argparse.nonnegativeInt(optarg, 'entity') end }, + { 'f', 'histfig', hasArg=true, handler=function(optarg) opts.histfig_id = argparse.nonnegativeInt(optarg, 'histfig') end }, { 'h', 'help', handler = function() opts.help = true end }, - { 'l', 'location', handler=function(optarg) opts.location_id = argparse.nonnegativeInt(optarg, 'location') end }, - { 'q', 'squad', handler=function(optarg) opts.squad_id = argparse.nonnegativeInt(optarg, 'squad') end }, - { 's', 'site', handler=function(optarg) opts.site_id = argparse.nonnegativeInt(optarg, 'site') end }, - { 'u', 'unit', handler=function(optarg) opts.unit_id = argparse.nonnegativeInt(optarg, 'unit') end }, + { 'l', 'location', hasArg=true, handler=function(optarg) opts.location_id = argparse.nonnegativeInt(optarg, 'location') end }, + { 'q', 'squad', hasArg=true, handler=function(optarg) opts.squad_id = argparse.nonnegativeInt(optarg, 'squad') end }, + { 's', 'site', hasArg=true, handler=function(optarg) opts.site_id = argparse.nonnegativeInt(optarg, 'site') end }, + { 'u', 'unit', hasArg=true, handler=function(optarg) opts.unit_id = argparse.nonnegativeInt(optarg, 'unit') end }, { 'w', 'world', handler=function() opts.world = true end }, { '', 'no-target-selector', handler=function() opts.show_selector = false end }, }) From add5d172d82b3d136529509a4963911694488975 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 13 Apr 2025 14:15:09 -0500 Subject: [PATCH 523/811] Update deathcause.lua --- deathcause.lua | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index d0a90dd6c9..0ea44d24e7 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -4,7 +4,7 @@ local DEATH_TYPES = reqscript('gui/unit-info-viewer').DEATH_TYPES -- Gets the first corpse item at the given location -function getItemAtPosition(pos) +local function getItemAtPosition(pos) for _, item in ipairs(df.global.world.items.other.ANY_CORPSE) do if item.pos.x == pos.x and item.pos.y == pos.y and item.pos.z == pos.z then print("Automatically chose first corpse at the selected location.") @@ -13,11 +13,11 @@ function getItemAtPosition(pos) end end -function getRaceNameSingular(race_id) +local function getRaceNameSingular(race_id) return df.creature_raw.find(race_id).name[0] end -function getDeathStringFromCause(cause) +local function getDeathStringFromCause(cause) if cause == -1 then return "died" else @@ -25,7 +25,7 @@ function getDeathStringFromCause(cause) end end -function displayDeathUnit(unit) +function getDeathUnit(unit) local str = unit.name.has_name and '' or 'The ' str = str .. dfhack.units.getReadableName(unit) @@ -63,7 +63,7 @@ function getWeaponName(item_id, subtype) return dfhack.items.getDescription(item, 0, false) end -function displayDeathEventHistFigUnit(histfig_unit, event) +function getDeathEventHistFigUnit(histfig_unit, event) local str = ("The %s %s %s in year %d"):format( getRaceNameSingular(histfig_unit.race), dfhack.translation.translateName(dfhack.units.getVisibleName(histfig_unit)), @@ -102,7 +102,7 @@ function getDeathEventForHistFig(histfig_id) end end -function displayDeathHistFig(histfig) +function getDeathHistFig(histfig) local histfig_unit = df.unit.find(histfig.unit_id) if not histfig_unit then qerror("Cause of death not available") @@ -112,7 +112,7 @@ function displayDeathHistFig(histfig) return ("%s is not dead yet!"):format(dfhack.units.getReadableName(histfig_unit)) else local death_event = getDeathEventForHistFig(histfig.id) - return displayDeathEventHistFigUnit(histfig_unit, death_event) + return getDeathEventHistFigUnit(histfig_unit, death_event) end end @@ -159,7 +159,7 @@ elseif hist_figure_id == -1 then if not selected_unit then qerror("Cause of death not available") end - print(dfhack.df2console(displayDeathUnit(selected_unit))) + print(dfhack.df2console(getDeathUnit(selected_unit))) else - print(dfhack.df2console(displayDeathHistFig(df.historical_figure.find(hist_figure_id)))) + print(dfhack.df2console(getDeathHistFig(df.historical_figure.find(hist_figure_id)))) end From c611d4efaa180b8093200029a858c1b05fa748bc Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 14 Apr 2025 11:35:39 -0700 Subject: [PATCH 524/811] fix inorganics raws scan --- changelog.txt | 1 + gui/sandbox.lua | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 55bfcf2f5c..bc4da721cd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: - `list-agreements`: fix date math when determining petition age - `gui/petitions`: fix date math when determining petition age - `gui/rename`: fix commandline processing when manually specifying target ids +- `gui/sandbox`: restore metal equipment options when spawning units ## Misc Improvements - `fix/loyaltycascade`: now also breaks up brawls and other intra-fort conflicts that *look* like loyalty cascades diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 29e93b51ce..2fed03ca87 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -458,7 +458,7 @@ local function init_arena() -- for i in ipairs(df.builtin_mats) do -- do_insert(MAT_TABLE.builtin[i], i, -1) -- end - for i, mat in ipairs(RAWS.inorganics) do + for i, mat in ipairs(RAWS.inorganics.all) do do_insert(mat.material, 0, i) -- stop at the first "special" metal. we don't need more than that if mat.flags.DEEP_SPECIAL then break end From 3977dd1b9829bc64d23f158bb65cb0d3e996f2f6 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 15 Apr 2025 19:12:23 -0700 Subject: [PATCH 525/811] Fix changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index ef4d58e27d..3274dd97b6 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `gui/adv-finder`: UI for tracking historical figures and artifacts in adventure mode ## New Features @@ -60,7 +61,6 @@ Template for new versions: - `autocheese`: automatically make cheese using barrels that have accumulated sufficient milk - `gui/spectate`: interactive UI for configuring `spectate` - `gui/notes`: UI for adding and managing notes attached to tiles on the map -- `gui/adv-finder`: UI for tracking historical figures and artifacts in adventure mode - `launch`: (reinstated) new adventurer fighting move: thrash your enemies with a flying suplex - `putontable`: (reinstated) make an item appear on a table From bedf4266c7b969c5d8031749f70b380ed9a0e719 Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Tue, 15 Apr 2025 19:15:28 -0700 Subject: [PATCH 526/811] adv-finder.lua - Improve site center pos --- gui/adv-finder.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/adv-finder.lua b/gui/adv-finder.lua index 9831b0b04e..cfd17b7646 100644 --- a/gui/adv-finder.lua +++ b/gui/adv-finder.lua @@ -288,8 +288,8 @@ function cz_g_pos(cz_id) --Creation zone center in global coords end function site_g_pos(site) --Site center in global coords (blocks from world origin) - local x, y = site.global_min_x*3, site.global_min_y*3 - x, y = x + (site.global_max_x*3 - x)//2, y + (site.global_max_y*3 - y)//2 + local x, y = site.global_min_x, site.global_min_y + x, y = (x + (site.global_max_x - x)//2)*3+1, (y + (site.global_max_y - y)//2)*3+1 return {x = x, y = y} end From a1770a269ed409379fef53695726b17ef07a35c3 Mon Sep 17 00:00:00 2001 From: Ryan Bennitt Date: Wed, 16 Apr 2025 15:03:56 +0100 Subject: [PATCH 527/811] Add n-point star to design shapes --- internal/design/shapes.lua | 147 +++++++++++++++++++++++++++++++++++-- 1 file changed, 141 insertions(+), 6 deletions(-) diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index 5199aa1d10..722cd66aae 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -402,7 +402,9 @@ function Diag:has_point(x, y) end end -Line = defclass(Line, Shape) +LineDrawer = defclass(LineDrawer, Shape) + +Line = defclass(Line, LineDrawer) Line.ATTRS { name = "Line", extra_points = { { label = "Curve Point" }, { label = "Second Curve Point" } }, @@ -431,18 +433,19 @@ function Line:init() } end -function Line:plot_bresenham(x0, y0, x1, y1, thickness) +function LineDrawer:plot_bresenham(x0, y0, x1, y1, thickness) local dx = math.abs(x1 - x0) local dy = math.abs(y1 - y0) local sx = x0 < x1 and 1 or -1 local sy = y0 < y1 and 1 or -1 - local err = dx - dy local e2, x, y for i = 0, thickness - 1 do x = x0 y = y0 + i - while true do + local err = dx - dy + local p = math.max(dx, dy) + while p >= 0 do for j = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do if not self.arr[x + j] then self.arr[x + j] = {} end if not self.arr[x + j][y] then @@ -451,7 +454,7 @@ function Line:plot_bresenham(x0, y0, x1, y1, thickness) end end - if x == x1 and y == y1 + i then + if sx * x >= sx * x1 and sy * y >= sy * (y1 + i) then break end @@ -466,6 +469,7 @@ function Line:plot_bresenham(x0, y0, x1, y1, thickness) err = err + dx y = y + sy end + p = p - 1 end end @@ -684,7 +688,138 @@ function FreeForm:point_in_polygon(x, y) return inside end +Star = defclass(Star, LineDrawer) +Star.ATTRS { + name = "Star", + texture_offset = 5, + button_chars = util.make_ascii_button(9, 248), + extra_points = { { label = "Main Axis" } } +} + +function Star:init() + self.options = { + hollow = { + name = "Hollow", + type = "bool", + value = false, + key = "CUSTOM_H", + }, + thickness = { + name = "Line thickness", + type = "plusminus", + value = 1, + enabled = { "hollow", true }, + min = 1, + max = function(shape) if not shape.height or not shape.width then + return nil + else + return math.ceil(math.min(shape.height, shape.width) / 2) + + end + end, + keys = { "CUSTOM_T", "CUSTOM_SHIFT_T" }, + }, + total_points = { + name = "Total points", + type = "plusminus", + value = 5, + min = 3, + max = 100, + keys = { "CUSTOM_B", "CUSTOM_SHIFT_B" }, + }, + next_point_offset = { + name = "Next point offset", + type = "plusminus", + value = 2, + min = 1, + max = 99, + keys = { "CUSTOM_N", "CUSTOM_SHIFT_N" }, + }, + } +end + +function Star:has_point(x, y) + if 1 < ((x-self.center.x) / self.center.x) ^ 2 + ((y-self.center.y) / self.center.y) ^ 2 then return false end + + local inside = 0 + for l = 1, self.options.total_points.value do + if x * self.lines[l].slope.x - self.lines[l].intercept.x < y * self.lines[l].slope.y - self.lines[l].intercept.y then + inside = inside + 1 + else + inside = inside - 1 + end + end + return self.threshold > 0 and inside > self.threshold or inside < self.threshold +end + +function vmagnitude(point) + return math.sqrt(point.x * point.x + point.y * point.y) +end + +function vnormalize(point) + local magnitude = vmagnitude(point) + return { x = point.x / magnitude, y = point.y / magnitude } +end + +function add_offset(coord, offset) + return math.tointeger(coord + (offset > 0 and math.floor(offset+0.5) or math.ceil(offset-0.5))) +end + +function Star:update(points, extra_points) + self.num_tiles = 0 + self.points = copyall(points) + self.arr = {} + if #points < self.min_points then return end + self.threshold = self.options.total_points.value - 2 * self.options.next_point_offset.value + if self.threshold < 0 then return end + local top_left, bot_right = self:get_point_dims() + self.height = bot_right.y - top_left.y + self.width = bot_right.x - top_left.x + if self.height == 1 or self.width == 1 then return end + self.center = { x = self.width * 0.5, y = self.height * 0.5 } + local axes = {} + + axes[1] = (#extra_points > 0) and { x = extra_points[1].x - self.center.x - top_left.x, y = extra_points[1].y - self.center.y - top_left.y } or { x = 0, y = -self.center.y } + if vmagnitude(axes[1]) < 0.5 then axes[1].y = -self.center.y end + axes[1] = vnormalize(axes[1]) + + for a = 2, self.options.total_points.value do + local angle = math.pi * (a - 1.0) * 2.0 / self.options.total_points.value + axes[a] = { x = math.cos(angle) * axes[1].x - math.sin(angle) * axes[1].y, y = math.sin(angle) * axes[1].x + math.cos(angle) * axes[1].y } + end + + local thickness = 1 + if self.options.hollow.value then + thickness = self.options.thickness.value + end + + self.lines = {} + for l = 1, self.options.total_points.value do + local p1 = { x = self.center.x + axes[l].x * self.width * 0.5, y = self.center.y + axes[l].y * self.height * 0.5 } + local next_axis = axes[(l-1+self.options.next_point_offset.value) % self.options.total_points.value + 1] + local p2 = { x = self.center.x + next_axis.x * self.width * 0.5, y = self.center.y + next_axis.y * self.height * 0.5 } + self.lines[l] = { slope = { x = p2.y - p1.y, y = p2.x - p1.x }, intercept = { x = (p2.y - p1.y) * p1.x, y = (p2.x - p1.x) * p1.y } } + self:plot_bresenham(add_offset(top_left.x, p1.x), add_offset(top_left.y, p1.y), add_offset(top_left.x, p2.x), add_offset(top_left.y, p2.y), thickness) + self:plot_bresenham(add_offset(top_left.x, p2.x), add_offset(top_left.y, p2.y), add_offset(top_left.x, p1.x), add_offset(top_left.y, p1.y), thickness) + end + + if not self.options.hollow.value then + for x = top_left.x, bot_right.x do + if not self.arr[x] then self.arr[x] = {} end + for y = top_left.y, bot_right.y do + local value = self:has_point(x - top_left.x, y - top_left.y) + if self.invert then + self.arr[x][y] = not self.arr[x][y] and not value + else + self.arr[x][y] = self.arr[x][y] or value + end + + self.num_tiles = self.num_tiles + (self.arr[x][y] and 1 or 0) + end + end + end +end -- module users can get shapes through this global, shape option values -- persist in these as long as the module is loaded -- idk enough lua to know if this is okay to do or not -all_shapes = { Rectangle {}, Ellipse {}, Rows {}, Diag {}, Line {}, FreeForm {} } +all_shapes = { Rectangle {}, Ellipse {}, Rows {}, Diag {}, Line {}, FreeForm {}, Star {} } From 0719f704aa2810d7f0d0cf99a4712bfdf5209929 Mon Sep 17 00:00:00 2001 From: Ryan Bennitt Date: Wed, 16 Apr 2025 16:53:01 +0100 Subject: [PATCH 528/811] Allow next point offset to increase indefinitely with !!fun!! results --- internal/design/shapes.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index 722cd66aae..652e94a7a5 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -732,7 +732,7 @@ function Star:init() type = "plusminus", value = 2, min = 1, - max = 99, + max = 100, keys = { "CUSTOM_N", "CUSTOM_SHIFT_N" }, }, } @@ -771,7 +771,6 @@ function Star:update(points, extra_points) self.arr = {} if #points < self.min_points then return end self.threshold = self.options.total_points.value - 2 * self.options.next_point_offset.value - if self.threshold < 0 then return end local top_left, bot_right = self:get_point_dims() self.height = bot_right.y - top_left.y self.width = bot_right.x - top_left.x From 9fd2bc7273cde94fdb4f302ef5964292ed6e90c4 Mon Sep 17 00:00:00 2001 From: 83N170 <83N170@mail.com> Date: Wed, 16 Apr 2025 20:36:17 +0100 Subject: [PATCH 529/811] Fix invert of hollow star --- internal/design/shapes.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index 652e94a7a5..8d673ee108 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -762,7 +762,7 @@ function vnormalize(point) end function add_offset(coord, offset) - return math.tointeger(coord + (offset > 0 and math.floor(offset+0.5) or math.ceil(offset-0.5))) + return coord + (offset > 0 and math.floor(offset+0.5) or math.ceil(offset-0.5)) end function Star:update(points, extra_points) @@ -802,15 +802,15 @@ function Star:update(points, extra_points) self:plot_bresenham(add_offset(top_left.x, p2.x), add_offset(top_left.y, p2.y), add_offset(top_left.x, p1.x), add_offset(top_left.y, p1.y), thickness) end - if not self.options.hollow.value then + if not self.options.hollow.value or self.invert then for x = top_left.x, bot_right.x do if not self.arr[x] then self.arr[x] = {} end for y = top_left.y, bot_right.y do - local value = self:has_point(x - top_left.x, y - top_left.y) + local value = self.arr[x][y] or (not self.options.hollow.value and self:has_point(x - top_left.x, y - top_left.y)) if self.invert then - self.arr[x][y] = not self.arr[x][y] and not value + self.arr[x][y] = not value else - self.arr[x][y] = self.arr[x][y] or value + self.arr[x][y] = value end self.num_tiles = self.num_tiles + (self.arr[x][y] and 1 or 0) From 771244cdcca7c322d71b4dbe910a01d395cff0df Mon Sep 17 00:00:00 2001 From: 83N170 <83N170@mail.com> Date: Fri, 18 Apr 2025 12:36:07 +0100 Subject: [PATCH 530/811] Use new star icon --- gui/design.lua | 6 +++--- internal/design/shapes.lua | 4 ++-- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index 103ab59c8e..e4573bb5fd 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -344,14 +344,14 @@ function Design:init() chars=shape.button_chars, tileset=shape_tileset, tileset_offset=shape.texture_offset, - tileset_stride=24, + tileset_stride=28, }) table.insert(shape_button_specs_selected, { chars=shape.button_chars, pens=COLOR_YELLOW, tileset=shape_tileset, - tileset_offset=shape.texture_offset+(24*3), - tileset_stride=24, + tileset_offset=shape.texture_offset+(28*3), + tileset_stride=28, }) end diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index 8d673ee108..ab5a5142f6 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -691,8 +691,8 @@ end Star = defclass(Star, LineDrawer) Star.ATTRS { name = "Star", - texture_offset = 5, - button_chars = util.make_ascii_button(9, 248), + texture_offset = 25, + button_chars = util.make_ascii_button('*', '*'), extra_points = { { label = "Main Axis" } } } From 1e787155338283036da49fa40c19ac3af5f07e87 Mon Sep 17 00:00:00 2001 From: 83N170 <83N170@mail.com> Date: Fri, 18 Apr 2025 15:56:39 +0100 Subject: [PATCH 531/811] Make icon calculations clearer and tweak ascii icon --- gui/design.lua | 15 +++++++++++---- internal/design/shapes.lua | 2 +- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index e4573bb5fd..5cd3f1d824 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -336,7 +336,14 @@ function Design:init() table.insert(mode_button_specs_selected, mode_option.value.button_selected_spec) end - local shape_tileset = dfhack.textures.loadTileset('hack/data/art/design.png', 8, 12, true) + local DESIGN_ICONS_WIDTH = 224 -- Must match design.png image width + local DESIGN_ICONS_HEIGHT = 72 -- Must match design.png image height + local DESIGN_ICON_ROW_COUNT = 2 + local DESIGN_CHAR_WIDTH = 8 + local DESIGN_CHAR_HEIGHT = 12 + local shape_tileset = dfhack.textures.loadTileset('hack/data/art/design.png', DESIGN_CHAR_WIDTH, DESIGN_CHAR_HEIGHT, true) + local STRIDE = DESIGN_ICONS_WIDTH / DESIGN_CHAR_WIDTH + local CHARS_PER_ROW = DESIGN_ICONS_HEIGHT / (DESIGN_ICON_ROW_COUNT * DESIGN_CHAR_HEIGHT) local shape_options, shape_button_specs, shape_button_specs_selected = {}, {}, {} for _, shape in ipairs(shapes.all_shapes) do table.insert(shape_options, {label=shape.name, value=shape}) @@ -344,14 +351,14 @@ function Design:init() chars=shape.button_chars, tileset=shape_tileset, tileset_offset=shape.texture_offset, - tileset_stride=28, + tileset_stride=STRIDE, }) table.insert(shape_button_specs_selected, { chars=shape.button_chars, pens=COLOR_YELLOW, tileset=shape_tileset, - tileset_offset=shape.texture_offset+(28*3), - tileset_stride=28, + tileset_offset=shape.texture_offset+(STRIDE*CHARS_PER_ROW), + tileset_stride=STRIDE, }) end diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index ab5a5142f6..a9eb9897d8 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -692,7 +692,7 @@ Star = defclass(Star, LineDrawer) Star.ATTRS { name = "Star", texture_offset = 25, - button_chars = util.make_ascii_button('*', '*'), + button_chars = util.make_ascii_button('*', 15), extra_points = { { label = "Main Axis" } } } From 6b2e4e4d7423335ac73b6842d5d9db705d250773 Mon Sep 17 00:00:00 2001 From: 83N170 <83N170@mail.com> Date: Fri, 18 Apr 2025 16:03:26 +0100 Subject: [PATCH 532/811] Update changelog --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index bc4da721cd..b6a96e85ab 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,6 +16,8 @@ Template for new versions: ## New Features +- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction + ## Fixes ## Misc Improvements From b86f9f03bdb8ca00afa17e6362ad18b4d7bd5ed4 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Fri, 18 Apr 2025 12:28:19 -0500 Subject: [PATCH 533/811] Update deathcause.lua localize some more functions and then rename the APIs and add comments --- deathcause.lua | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index 0ea44d24e7..6c212821ac 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -25,7 +25,8 @@ local function getDeathStringFromCause(cause) end end -function getDeathUnit(unit) +-- Returns a cause of death given a unit +function getDeathCauseFromUnit(unit) local str = unit.name.has_name and '' or 'The ' str = str .. dfhack.units.getReadableName(unit) @@ -55,7 +56,7 @@ end -- returns the item description if the item still exists; otherwise -- returns the weapon name -function getWeaponName(item_id, subtype) +local function getWeaponName(item_id, subtype) local item = df.item.find(item_id) if not item then return df.global.world.raws.itemdefs.weapons[subtype].name @@ -63,7 +64,7 @@ function getWeaponName(item_id, subtype) return dfhack.items.getDescription(item, 0, false) end -function getDeathEventHistFigUnit(histfig_unit, event) +local function getDeathEventHistFigUnit(histfig_unit, event) local str = ("The %s %s %s in year %d"):format( getRaceNameSingular(histfig_unit.race), dfhack.translation.translateName(dfhack.units.getVisibleName(histfig_unit)), @@ -91,7 +92,7 @@ function getDeathEventHistFigUnit(histfig_unit, event) end -- Returns the death event for the given histfig or nil if not found -function getDeathEventForHistFig(histfig_id) +local function getDeathEventForHistFig(histfig_id) for i = #df.global.world.history.events - 1, 0, -1 do local event = df.global.world.history.events[i] if event:getType() == df.history_event_type.HIST_FIGURE_DIED then @@ -102,7 +103,8 @@ function getDeathEventForHistFig(histfig_id) end end -function getDeathHistFig(histfig) +-- Returns the cause of death given a histfig +function getDeathCauseFromHistFig(histfig) local histfig_unit = df.unit.find(histfig.unit_id) if not histfig_unit then qerror("Cause of death not available") @@ -159,7 +161,7 @@ elseif hist_figure_id == -1 then if not selected_unit then qerror("Cause of death not available") end - print(dfhack.df2console(getDeathUnit(selected_unit))) + print(dfhack.df2console(getDeathCauseFromUnit(selected_unit))) else - print(dfhack.df2console(getDeathHistFig(df.historical_figure.find(hist_figure_id)))) + print(dfhack.df2console(getDeathCauseFromHistFig(df.historical_figure.find(hist_figure_id)))) end From aade3cf31698f60491c68729ed2a5687fe1b1d18 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:38:09 -0700 Subject: [PATCH 534/811] use new assigned_unit API for 51.11 --- fix/ownership.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fix/ownership.lua b/fix/ownership.lua index c98dd42476..b3efd612bf 100644 --- a/fix/ownership.lua +++ b/fix/ownership.lua @@ -33,7 +33,7 @@ local zone_vecs = { local function relink_zones() for _,zones in ipairs(zone_vecs) do for _,zone in ipairs(zones) do - local unit = zone.assigned_unit + local unit = dfhack.buildings.getOwner(zone) if not unit then goto continue end if not utils.linear_index(unit.owned_buildings, zone.id, 'id') then print(('fix/ownership: Restoring %s ownership link for %s'):format( From b5178d671f9e6142c7c3fccadf025e7eb07d9b04 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 21 Apr 2025 09:48:09 -0700 Subject: [PATCH 535/811] update changelog --- changelog.txt | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index bc4da721cd..9b676acc1e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,14 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Removed + +# 51.11-r1 + ## Fixes - `list-agreements`: fix date math when determining petition age - `gui/petitions`: fix date math when determining petition age @@ -40,8 +48,6 @@ Template for new versions: - `fix/loyaltycascade`: now also breaks up brawls and other intra-fort conflicts that *look* like loyalty cascades - `makeown`: remove selected unit from any current conflicts so they don't just start attacking other citizens when you make them a citizen of your fort -## Removed - # 51.09-r1 ## New Features From 404f4925aceeb7d125c52c04906e261e69ab39eb Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 25 Apr 2025 15:59:10 -0700 Subject: [PATCH 536/811] properly handle state loading and ensure we don't decay too often --- changelog.txt | 2 + docs/starvingdead.rst | 6 +- emigration.lua | 23 +++-- starvingdead.lua | 217 +++++++++++++++++++++++------------------- 4 files changed, 139 insertions(+), 109 deletions(-) diff --git a/changelog.txt b/changelog.txt index 9b676acc1e..ff710b92aa 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,8 @@ Template for new versions: ## New Features ## Fixes +- `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session +- `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game ## Misc Improvements diff --git a/docs/starvingdead.rst b/docs/starvingdead.rst index 12f7efa14c..84faaf9509 100644 --- a/docs/starvingdead.rst +++ b/docs/starvingdead.rst @@ -10,11 +10,11 @@ gradually decay, losing strength, speed, and toughness. After six months, they collapse upon themselves, never to be reanimated. Strength lost is proportional to the time until death, all units will have -roughly 10% of each of their attributes' values when close to being removed. +roughly 10% of each of their attributes' values when they succumb to decay. In any game, this can be a welcome gameplay feature, but it is especially -useful in preventing undead cascades in the caverns in reanimating biomes, -where constant combat can lead to hundreds of undead roaming the caverns and +useful in preventing undead cascades in the caverns in reanimating biomes. +Constant combat can lead to hundreds of undead roaming the caverns and destroying your FPS. Usage diff --git a/emigration.lua b/emigration.lua index 1bba9810e1..fd18f21293 100644 --- a/emigration.lua +++ b/emigration.lua @@ -9,7 +9,10 @@ local unit_link_utils = reqscript('internal/emigration/unit-link-utils') local GLOBAL_KEY = 'emigration' -- used for state change hooks and persistence local function get_default_state() - return {enabled=false, last_cycle_tick=0} + return { + enabled=false, + last_cycle_tick=0 + } end state = state or get_default_state() @@ -127,15 +130,15 @@ function checkmigrationnow() end local function event_loop() - if state.enabled then - local current_tick = dfhack.world.ReadCurrentTick() + TICKS_PER_YEAR * dfhack.world.ReadCurrentYear() - if current_tick - state.last_cycle_tick < TICKS_PER_MONTH then - local timeout_ticks = state.last_cycle_tick - current_tick + TICKS_PER_MONTH - dfhack.timeout(timeout_ticks, 'ticks', event_loop) - else - checkmigrationnow() - dfhack.timeout(1, 'months', event_loop) - end + if not state.enabled then return end + + local current_tick = dfhack.world.ReadCurrentTick() + TICKS_PER_YEAR * dfhack.world.ReadCurrentYear() + if current_tick - state.last_cycle_tick < TICKS_PER_MONTH then + local timeout_ticks = state.last_cycle_tick - current_tick + TICKS_PER_MONTH + dfhack.timeout(timeout_ticks, 'ticks', event_loop) + else + checkmigrationnow() + dfhack.timeout(1, 'months', event_loop) end end diff --git a/starvingdead.lua b/starvingdead.lua index 42742d5a28..5518676ad1 100644 --- a/starvingdead.lua +++ b/starvingdead.lua @@ -3,128 +3,153 @@ --@module = true local argparse = require('argparse') +local utils = require('utils') local GLOBAL_KEY = 'starvingdead' -starvingDeadInstance = starvingDeadInstance or nil +local function get_default_state() + return { + enabled=false, + decay_rate=1, + death_threshold=6, + last_cycle_tick=0, + } +end + +state = state or get_default_state() function isEnabled() - return starvingDeadInstance ~= nil + return state.enabled end local function persist_state() - dfhack.persistent.saveSiteData(GLOBAL_KEY, { - enabled = isEnabled(), - decay_rate = starvingDeadInstance and starvingDeadInstance.decay_rate or 1, - death_threshold = starvingDeadInstance and starvingDeadInstance.death_threshold or 6 - }) + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) end -dfhack.onStateChange[GLOBAL_KEY] = function(sc) - if sc == SC_MAP_UNLOADED then - enabled = false - return - end - - if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then - return - end - - local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) - - if persisted_data.enabled then - starvingDeadInstance = StarvingDead{ - decay_rate = persisted_data.decay_rate, - death_threshold = persisted_data.death_threshold - } - end -end - -StarvingDead = defclass(StarvingDead) -StarvingDead.ATTRS{ - decay_rate = 1, - death_threshold = 6, -} - -function StarvingDead:init() - self.timeout_id = nil - -- Percentage goal each attribute should reach before death. - local attribute_goal = 10 - self.attribute_decay = (attribute_goal ^ (1 / ((self.death_threshold * 28 / self.decay_rate)))) / 100 - - self:checkDecay() - print(([[StarvingDead started, checking every %s days and killing off at %s months]]):format(self.decay_rate, self.death_threshold)) -end - -function StarvingDead:checkDecay() - for _, unit in pairs(df.global.world.units.active) do - if (unit.enemy.undead and not unit.flags1.inactive) then - -- time_on_site is measured in ticks, a month is 33600 ticks. - -- @see https://dwarffortresswiki.org/index.php/Time - for _, attribute in pairs(unit.body.physical_attrs) do - attribute.value = math.floor(attribute.value - (attribute.value * self.attribute_decay)) - end - - if unit.curse.interaction.time_on_site > (self.death_threshold * 33600) then - unit.animal.vanish_countdown = 1 - end +-- threshold each attribute should reach before death. +local ATTRIBUTE_THRESHOLD_PERCENT = 10 + +local TICKS_PER_DAY = 1200 +local TICKS_PER_MONTH = 28 * TICKS_PER_DAY +local TICKS_PER_YEAR = 12 * TICKS_PER_MONTH + +local function do_decay() + local decay_exponent = state.decay_rate / (state.death_threshold * 28) + local attribute_decay = (ATTRIBUTE_THRESHOLD_PERCENT ^ decay_exponent) / 100 + + for _, unit in pairs(df.global.world.units.active) do + if (unit.enemy.undead and not unit.flags1.inactive) then + for _,attribute in pairs(unit.body.physical_attrs) do + attribute.value = math.floor(attribute.value - (attribute.value * attribute_decay)) + end + + if unit.curse.interaction.time_on_site > (state.death_threshold * TICKS_PER_MONTH) then + unit.animal.vanish_countdown = 1 + end + end end - end +end - self.timeout_id = dfhack.timeout(self.decay_rate, 'days', self:callback('checkDecay')) +local function get_normalized_tick() + return dfhack.world.ReadCurrentTick() + TICKS_PER_YEAR * dfhack.world.ReadCurrentYear() end -if dfhack_flags.module then - return +timeout_id = timeout_id or nil + +local function event_loop() + if not state.enabled then return end + + local current_tick = get_normalized_tick() + local ticks_per_cycle = TICKS_PER_DAY * state.decay_rate + local timeout_ticks = ticks_per_cycle + + if current_tick - state.last_cycle_tick < ticks_per_cycle then + timeout_ticks = state.last_cycle_tick - current_tick + ticks_per_cycle + else + do_decay() + state.last_cycle_tick = current_tick + persist_state() + end + timeout_id = dfhack.timeout(timeout_ticks, 'ticks', event_loop) end -local options, args = { - decay_rate = nil, - death_threshold = nil -}, {...} +local function do_enable() + if state.enabled then return end -local positionals = argparse.processArgsGetopt(args, { - {'h', 'help', handler = function() options.help = true end}, - {'r', 'decay-rate', hasArg = true, handler=function(arg) options.decay_rate = argparse.positiveInt(arg, 'decay-rate') end }, - {'t', 'death-threshold', hasArg = true, handler=function(arg) options.death_threshold = argparse.positiveInt(arg, 'death-threshold') end }, -}) + state.enabled = true + state.last_cycle_tick = get_normalized_tick() + event_loop() +end -if dfhack_flags.enable then - if dfhack_flags.enable_state then - if starvingDeadInstance then - return +local function do_disable() + if not state.enabled then return end + + state.enabled = false + if timeout_id then + dfhack.timeout_active(timeout_id, nil) + timeout_id = nil end +end - starvingDeadInstance = StarvingDead{} - persist_state() - else - if not starvingDeadInstance then - return +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + do_disable() + return end - dfhack.timeout_active(starvingDeadInstance.timeout_id, nil) - starvingDeadInstance = nil - end -else - if not dfhack.isMapLoaded() then - qerror('This script requires a fortress map to be loaded') - end + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return + end - if positionals[1] == "help" or options.help then - print(dfhack.script_help()) + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) + + event_loop() +end + +if dfhack_flags.module then return - end +end - if positionals[1] == nil then - if starvingDeadInstance then - starvingDeadInstance.decay_rate = options.decay_rate or starvingDeadInstance.decay_rate - starvingDeadInstance.death_threshold = options.death_threshold or starvingDeadInstance.death_threshold +if not dfhack.isMapLoaded() or not dfhack.world.isFortressMode() then + qerror('This script requires a fortress map to be loaded') +end - print(([[StarvingDead is running, checking every %s days and killing off at %s months]]):format( - starvingDeadInstance.decay_rate, starvingDeadInstance.death_threshold - )) +if dfhack_flags.enable then + if dfhack_flags.enable_state then + do_enable() else - print("StarvingDead is not running!") + do_disable() end - end +end + +local opts = {} +local positionals = argparse.processArgsGetopt({...}, { + {'h', 'help', handler=function() opts.help = true end}, + {'r', 'decay-rate', hasArg=true, + handler=function(arg) opts.decay_rate = argparse.positiveInt(arg, 'decay-rate') end }, + {'t', 'death-threshold', hasArg=true, + handler=function(arg) opts.death_threshold = argparse.positiveInt(arg, 'death-threshold') end }, +}) + + +if positionals[1] == "help" or opts.help then + print(dfhack.script_help()) + return +end + +if opts.decay_rate then + state.decay_rate = opts.decay_rate +end +if opts.death_threshold then + state.death_threshold = opts.death_threshold +end +persist_state() + +if state.enabled then + print(([[StarvingDead is running, decaying undead every %s day%s and killing off at %s month%s]]):format( + state.decay_rate, state.decay_rate == 1 and '' or 's', state.death_threshold, state.death_threshold == 1 and '' or 's')) +else + print(([[StarvingDead is not running, but would decay undead every %s day%s and kill off at %s month%s]]):format( + state.decay_rate, state.decay_rate == 1 and '' or 's', state.death_threshold, state.death_threshold == 1 and '' or 's')) end From 3ea6b8811c13e6084ca92230bd5b789d428dfb88 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Fri, 25 Apr 2025 20:52:04 -0500 Subject: [PATCH 537/811] `gui/mod-manager`: Add a window showing the active mods in a world (#1431) * add gui/mod-manager UI --------- Signed-off-by: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Co-authored-by: Myk Co-authored-by: Myk Taylor --- changelog.txt | 1 + docs/gui/mod-manager.rst | 24 +++++- gui/mod-manager.lua | 165 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 181 insertions(+), 9 deletions(-) diff --git a/changelog.txt b/changelog.txt index ff710b92aa..26badf3a66 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools ## New Features +- `gui/mod-manager`: when run in a loaded world, shows a list of active mods -- click to export the list to the clipboard for easy sharing or posting ## Fixes - `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session diff --git a/docs/gui/mod-manager.rst b/docs/gui/mod-manager.rst index 8972fece72..56b1538f29 100644 --- a/docs/gui/mod-manager.rst +++ b/docs/gui/mod-manager.rst @@ -2,11 +2,11 @@ gui/mod-manager =============== .. dfhack-tool:: - :summary: Save and restore lists of active mods. + :summary: Manange your active mods. :tags: dfhack interface -Adds an optional overlay to the mod list screen that allows you to save and -load mod list presets, as well as set a default mod list preset for new worlds. +When run with a world loaded, shows a list of active mods. You can copy the +list to the system clipboard for easy sharing or posting. Usage ----- @@ -14,3 +14,21 @@ Usage :: gui/mod-manager + +Overlay +------- + +This tool also provides two overlays that are managed by the `overlay` +framework. + +gui/mod-manager.button +~~~~~~~~~~~~~~~~~~~~~~ + +Adds a widget to the mod list screen that allows you to save and load mod list +presets. You can also set a default mod list preset for new worlds so you don't +have to manualy re-select the same mods every time you generate a world. + +gui/mod-manager.notification +~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Displays a message when a mod preset has been auto-applied. diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 51df00d677..ee3a489767 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -1,12 +1,13 @@ --- Save and restore lists of active mods. +-- Show, save, and restore lists of active mods. --@ module = true -local overlay = require('plugins.overlay') -local gui = require('gui') -local widgets = require('gui.widgets') local dialogs = require('gui.dialogs') +local gui = require('gui') local json = require('json') +local overlay = require('plugins.overlay') +local scriptmanager = require('script-manager') local utils = require('utils') +local widgets = require('gui.widgets') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' @@ -119,6 +120,9 @@ local function swap_modlist(viewscreen, modlist) return failures end +-------------------- +-- ModmanageMenu + ModmanageMenu = defclass(ModmanageMenu, widgets.Window) ModmanageMenu.ATTRS { view_id = "modman_menu", @@ -389,6 +393,9 @@ function ModmanageMenu:init() } end +-------------------- +-- ModmanageScreen + ModmanageScreen = defclass(ModmanageScreen, gui.ZScreen) ModmanageScreen.ATTRS { focus_path = "mod-manager", @@ -401,6 +408,149 @@ function ModmanageScreen:init() } end +-------------------- +-- ModlistWindow + +ModlistWindow = defclass(ModlistWindow, widgets.Window) +ModlistWindow.ATTRS{ + frame_title="Active Mods", + frame={w=55, h=20}, + resizable=true, +} + +local function get_num_vanilla_mods() + local count = 0 + for _,mod in ipairs(scriptmanager.get_active_mods()) do + if mod.vanilla then + count = count + 1 + end + end + return count +end + +local function get_num_non_vanilla_mods() + local count = 0 + for _,mod in ipairs(scriptmanager.get_active_mods()) do + if not mod.vanilla then + count = count + 1 + end + end + return count +end + +function ModlistWindow:init() + self:addviews{ + widgets.CycleHotkeyLabel{ + view_id='vanilla', + frame={l=0, t=0, w=24}, + key='CUSTOM_V', + label='Vanilla mods:', + options={ + {label='Include', value=true, pen=COLOR_LIGHTBLUE}, + {label='Exclude', value=false, pen=COLOR_LIGHTRED}, + }, + initial_option=false, + on_change=function() self:refresh_list() end, + }, + widgets.HotkeyLabel{ + frame={t=0, r=0}, + label='Copy list to clipboard', + text_pen=COLOR_YELLOW, + auto_width=true, + on_activate=function() + local text = {} + for _,choice in ipairs(self.subviews.list:getChoices()) do + table.insert(text, choice.export_text) + end + dfhack.internal.setClipboardTextCp437Multiline(table.concat(text, NEWLINE)) + end, + enabled=function() return #self.subviews.list:getChoices() > 0 end, + }, + widgets.Divider{ + frame={t=2, h=1}, + frame_style=gui.FRAME_THIN, + frame_style_l=false, + frame_style_r=false, + }, + widgets.Label{ + frame={l=0, t=3}, + text={ + 'Load', + NEWLINE, + 'order', + }, + }, + widgets.Label{ + frame={l=7, t=4}, + text='Mod', + }, + widgets.List{ + view_id='list', + frame={t=6, b=2}, + }, + widgets.Label{ + frame={l=0, b=0}, + text={ + {text=('%d'):format(get_num_vanilla_mods()), pen=COLOR_LIGHTBLUE}, + ' vanilla mods', + {text=function() return self.subviews.vanilla:getOptionValue() and '' or ' (hidden)' end}, + ', ', + {text=('%d'):format(get_num_non_vanilla_mods()), pen=COLOR_BROWN}, + ' non-vanilla mods', + }, + }, + } + + self:refresh_list() +end + +function ModlistWindow:refresh_list() + local include_vanilla = self.subviews.vanilla:getOptionValue() + + local choices = {} + for idx,mod in ipairs(scriptmanager.get_active_mods()) do + if not include_vanilla and mod.vanilla then goto continue end + local steam_id = scriptmanager.get_mod_info_metadata(mod.path, 'STEAM_FILE_ID').STEAM_FILE_ID + local url = steam_id and (': https://steamcommunity.com/sharedfiles/filedetails/?id=%s'):format(steam_id) or '' + table.insert(choices, { + text={ + {text=idx, width=2, rjustify=true}, + ') ', + {text=mod.name, gap=3}, + ' (', + {text=mod.version, pen=COLOR_LIGHTGREEN}, + ')', + }, + data=mod, + export_text=('- %s (%s)%s'):format(mod.name, mod.version, url), + }) + ::continue:: + end + + self.subviews.list:setChoices(choices) +end + +-------------------- +-- ModlistScreen + +ModlistScreen = defclass(ModlistScreen, gui.ZScreen) +ModlistScreen.ATTRS{ + focus_path="mod-manager", +} + +function ModlistScreen:init() + self:addviews{ + ModlistWindow{} + } +end + +function ModlistScreen:onDismiss() + view = nil +end + +-------------------- +-- Overlays + ModmanageOverlay = defclass(ModmanageOverlay, overlay.OverlayWidget) ModmanageOverlay.ATTRS { frame = { w=16, h=3 }, @@ -494,5 +644,8 @@ if dfhack_flags.module then return end --- TODO: when invoked as a command, should show information on which mods are loaded --- and give the player the option to export the list (or at least copy it to the clipboard) +if not dfhack.isWorldLoaded() then + qerror("Please load a game before using the mod manager to see active mods.") +end + +view = view and view:raise() or ModlistScreen{}:show() From be763fa11cb198fb17cf37765bb8e7cd5afe9177 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 26 Apr 2025 11:27:29 -0700 Subject: [PATCH 538/811] adapt to longer strings returned from getReadableName --- gui/family-affairs.lua | 2 +- gui/manipulator.lua | 4 ++-- gui/sitemap.lua | 2 +- gui/unit-info-viewer.lua | 16 +++------------- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/gui/family-affairs.lua b/gui/family-affairs.lua index d67c9c0c0f..428e34d591 100644 --- a/gui/family-affairs.lua +++ b/gui/family-affairs.lua @@ -777,7 +777,7 @@ end FamilyAffairs = defclass(FamilyAffairs, widgets.Window) FamilyAffairs.ATTRS { frame_title='Family manager', - frame={w=50, h=30, r=2, t=18}, + frame={w=65, h=30, r=2, t=18}, frame_inset={t=1, l=1, r=1}, resizable=true, initial_tab=DEFAULT_NIL, diff --git a/gui/manipulator.lua b/gui/manipulator.lua index f7135412dd..82c699ef16 100644 --- a/gui/manipulator.lua +++ b/gui/manipulator.lua @@ -709,11 +709,11 @@ function Spreadsheet:init() }, DataColumn{ view_id='name', - frame={w=30}, + frame={w=45}, label='Name', label_inset=8, data_fn=dfhack.units.getReadableName, - data_width=30, + data_width=45, shared=self.shared, }, cols, diff --git a/gui/sitemap.lua b/gui/sitemap.lua index 576966f44c..b5d3974ccf 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -18,7 +18,7 @@ end Sitemap = defclass(Sitemap, widgets.Window) Sitemap.ATTRS { frame_title='Sitemap', - frame={w=57, r=2, t=18, h=25}, + frame={w=67, r=2, t=18, h=25}, resizable=true, resize_min={w=44, h=20}, frame_inset={l=1, t=1, r=0, b=0}, diff --git a/gui/unit-info-viewer.lua b/gui/unit-info-viewer.lua index d6c817e69b..419fb4f82f 100644 --- a/gui/unit-info-viewer.lua +++ b/gui/unit-info-viewer.lua @@ -179,12 +179,6 @@ local function get_name_chunk(unit) } end -local function get_translated_name_chunk(unit) - local tname = dfhack.translation.translateName(dfhack.units.getVisibleName(unit), true) - if #tname == 0 then return '' end - return ('"%s"'):format(tname) -end - local function get_description_chunk(unit) local desc = dfhack.units.getCasteRaw(unit).description if #desc == 0 then return end @@ -458,15 +452,12 @@ function UnitInfo:init() self:addviews{ widgets.Label{ view_id='nameprof', - frame={t=0, l=0}, - }, - widgets.Label{ - view_id='translated_name', - frame={t=1, l=0}, + frame={t=0, l=0, h=1}, + auto_height=false, }, widgets.Label{ view_id='chunks', - frame={t=3, l=0, b=0, r=0}, + frame={t=2, l=0, b=0, r=0}, auto_height=false, text='Please select a unit.', }, @@ -492,7 +483,6 @@ end function UnitInfo:refresh(unit, width) self.unit_id = unit.id self.subviews.nameprof:setText{get_name_chunk(unit)} - self.subviews.translated_name:setText{get_translated_name_chunk(unit)} local chunks = {} add_chunk(chunks, get_description_chunk(unit), width) From 6c24dc15f3268ee6216af3b8da565348888d2393 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 26 Apr 2025 12:30:58 -0700 Subject: [PATCH 539/811] support single-line mod list export --- gui/mod-manager.lua | 36 +++++++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 11 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index ee3a489767..792715c6d1 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -452,9 +452,29 @@ function ModlistWindow:init() initial_option=false, on_change=function() self:refresh_list() end, }, + widgets.Divider{ + frame={t=2, h=1}, + frame_style=gui.FRAME_THIN, + frame_style_l=false, + frame_style_r=false, + }, + widgets.HotkeyLabel{ + frame={t=4, r=0}, + label='Export to clipboard (single line)', + text_pen=COLOR_YELLOW, + auto_width=true, + on_activate=function() + local text = {} + for _,choice in ipairs(self.subviews.list:getChoices()) do + table.insert(text, ('%s %s'):format(choice.data.name, choice.data.version)) + end + dfhack.internal.setClipboardTextCp437Multiline(table.concat(text, ', ')) + end, + enabled=function() return #self.subviews.list:getChoices() > 0 end, + }, widgets.HotkeyLabel{ - frame={t=0, r=0}, - label='Copy list to clipboard', + frame={t=5, r=0}, + label='Export to clipboard (with links)', text_pen=COLOR_YELLOW, auto_width=true, on_activate=function() @@ -466,14 +486,8 @@ function ModlistWindow:init() end, enabled=function() return #self.subviews.list:getChoices() > 0 end, }, - widgets.Divider{ - frame={t=2, h=1}, - frame_style=gui.FRAME_THIN, - frame_style_l=false, - frame_style_r=false, - }, widgets.Label{ - frame={l=0, t=3}, + frame={l=0, t=4}, text={ 'Load', NEWLINE, @@ -481,12 +495,12 @@ function ModlistWindow:init() }, }, widgets.Label{ - frame={l=7, t=4}, + frame={l=7, t=5}, text='Mod', }, widgets.List{ view_id='list', - frame={t=6, b=2}, + frame={t=7, b=2}, }, widgets.Label{ frame={l=0, b=0}, From 90581cea9b9d652ec5bd09c2b0dff83e9df2c707 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 11:36:27 -0700 Subject: [PATCH 540/811] refine display of readable names gui/sitemap now has 2 rows per unit --- gui/sitemap.lua | 9 ++++----- gui/unit-info-viewer.lua | 16 ++++++++++++++-- prioritize.lua | 2 +- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/gui/sitemap.lua b/gui/sitemap.lua index b5d3974ccf..5af213a45b 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -18,7 +18,7 @@ end Sitemap = defclass(Sitemap, widgets.Window) Sitemap.ATTRS { frame_title='Sitemap', - frame={w=67, r=2, t=18, h=25}, + frame={w=67, r=2, t=18, h=26}, resizable=true, resize_min={w=44, h=20}, frame_inset={l=1, t=1, r=0, b=0}, @@ -141,12 +141,10 @@ end local function get_unit_choice_text(unit) local disposition, disposition_pen, affiliation = get_unit_disposition_and_pen_and_affiliation(unit) return { - dfhack.units.getReadableName(unit), - ' (', - {text=disposition, pen=disposition_pen}, + dfhack.units.getReadableName(unit), NEWLINE, + {gap=2, text=disposition, pen=disposition_pen}, affiliation and ': ' or '', {text=affiliation, pen=COLOR_YELLOW}, - ')', } end @@ -277,6 +275,7 @@ function Sitemap:init() }, widgets.FilteredList{ view_id='list', + row_height=2, on_submit=zoom_to_unit, on_submit2=follow_unit, choices=unit_choices, diff --git a/gui/unit-info-viewer.lua b/gui/unit-info-viewer.lua index 419fb4f82f..db1635a0a6 100644 --- a/gui/unit-info-viewer.lua +++ b/gui/unit-info-viewer.lua @@ -174,11 +174,17 @@ end local function get_name_chunk(unit) return { - text=dfhack.units.getReadableName(unit), + text=dfhack.units.getReadableName(unit, true), pen=dfhack.units.getProfessionColor(unit) } end +local function get_translated_name_chunk(unit) + local tname = dfhack.translation.translateName(dfhack.units.getVisibleName(unit), true) + if #tname == 0 then return '' end + return ('"%s"'):format(tname) +end + local function get_description_chunk(unit) local desc = dfhack.units.getCasteRaw(unit).description if #desc == 0 then return end @@ -455,9 +461,14 @@ function UnitInfo:init() frame={t=0, l=0, h=1}, auto_height=false, }, + widgets.Label{ + view_id='translated_name', + frame={t=1, l=0, h=1}, + auto_height=false, + }, widgets.Label{ view_id='chunks', - frame={t=2, l=0, b=0, r=0}, + frame={t=3, l=0, b=0, r=0}, auto_height=false, text='Please select a unit.', }, @@ -483,6 +494,7 @@ end function UnitInfo:refresh(unit, width) self.unit_id = unit.id self.subviews.nameprof:setText{get_name_chunk(unit)} + self.subviews.translated_name:setText{get_translated_name_chunk(unit)} local chunks = {} add_chunk(chunks, get_description_chunk(unit), width) diff --git a/prioritize.lua b/prioritize.lua index bd52d5ff09..5aa9857b1c 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -703,7 +703,7 @@ end function EnRouteOverlay:get_builder_name() if not self.builder then return 'N/A' end - return dfhack.units.getReadableName(self.builder) + return dfhack.units.getReadableName(self.builder, true) end function EnRouteOverlay:get_builder_name_pen() From 77eec26cb21d072d1bb845c00a771a091a1b9884 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 12:10:49 -0700 Subject: [PATCH 541/811] tone down color of affiliation text --- gui/sitemap.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/sitemap.lua b/gui/sitemap.lua index 5af213a45b..48a7a8d040 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -144,7 +144,7 @@ local function get_unit_choice_text(unit) dfhack.units.getReadableName(unit), NEWLINE, {gap=2, text=disposition, pen=disposition_pen}, affiliation and ': ' or '', - {text=affiliation, pen=COLOR_YELLOW}, + {text=affiliation, pen=COLOR_BROWN}, } end From 3f82177a1520ca72ddc4f2139ffaf86b89de407d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 12:14:14 -0700 Subject: [PATCH 542/811] recolor merchant units with unused color --- gui/sitemap.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/sitemap.lua b/gui/sitemap.lua index 48a7a8d040..9d3932794a 100644 --- a/gui/sitemap.lua +++ b/gui/sitemap.lua @@ -133,7 +133,7 @@ local function get_unit_disposition_and_pen_and_affiliation(unit) elseif dfhack.units.isVisitor(unit) or dfhack.units.isDiplomat(unit) then return prefix..'visitor', COLOR_MAGENTA, get_affiliation(unit) elseif dfhack.units.isMerchant(unit) or dfhack.units.isForest(unit) then - return prefix..'merchant'..(dfhack.units.isAnimal(unit) and ' animal' or ''), COLOR_BROWN, get_affiliation(unit) + return prefix..'merchant'..(dfhack.units.isAnimal(unit) and ' animal' or ''), COLOR_BLUE, get_affiliation(unit) end return prefix..'friendly', COLOR_LIGHTGREEN end From fdd597e75904f54c089bec114b3c8f5fa34ce520 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 12:21:29 -0700 Subject: [PATCH 543/811] light cleanup --- gui/design.lua | 2 ++ internal/design/shapes.lua | 59 +++++++++++++++++++------------------- 2 files changed, 31 insertions(+), 30 deletions(-) diff --git a/gui/design.lua b/gui/design.lua index 5cd3f1d824..8494fe111c 100644 --- a/gui/design.lua +++ b/gui/design.lua @@ -342,8 +342,10 @@ function Design:init() local DESIGN_CHAR_WIDTH = 8 local DESIGN_CHAR_HEIGHT = 12 local shape_tileset = dfhack.textures.loadTileset('hack/data/art/design.png', DESIGN_CHAR_WIDTH, DESIGN_CHAR_HEIGHT, true) + local STRIDE = DESIGN_ICONS_WIDTH / DESIGN_CHAR_WIDTH local CHARS_PER_ROW = DESIGN_ICONS_HEIGHT / (DESIGN_ICON_ROW_COUNT * DESIGN_CHAR_HEIGHT) + local shape_options, shape_button_specs, shape_button_specs_selected = {}, {}, {} for _, shape in ipairs(shapes.all_shapes) do table.insert(shape_options, {label=shape.name, value=shape}) diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index a9eb9897d8..34e8796c42 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -404,35 +404,6 @@ end LineDrawer = defclass(LineDrawer, Shape) -Line = defclass(Line, LineDrawer) -Line.ATTRS { - name = "Line", - extra_points = { { label = "Curve Point" }, { label = "Second Curve Point" } }, - invertable = false, -- Doesn't support invert - basic_shape = false, -- Driven by points, not rectangle bounds - texture_offset = 17, - button_chars = util.make_ascii_button(250, '(') -} - -function Line:init() - self.options = { - thickness = { - name = "Line thickness", - type = "plusminus", - value = 1, - min = 1, - max = function(shape) if not shape.height or not shape.width then - return nil - else - return math.max(shape.height, shape.width) - - end - end, - keys = { "CUSTOM_T", "CUSTOM_SHIFT_T" }, - }, - } -end - function LineDrawer:plot_bresenham(x0, y0, x1, y1, thickness) local dx = math.abs(x1 - x0) local dy = math.abs(y1 - y0) @@ -472,7 +443,35 @@ function LineDrawer:plot_bresenham(x0, y0, x1, y1, thickness) p = p - 1 end end +end + +Line = defclass(Line, LineDrawer) +Line.ATTRS { + name = "Line", + extra_points = { { label = "Curve Point" }, { label = "Second Curve Point" } }, + invertable = false, -- Doesn't support invert + basic_shape = false, -- Driven by points, not rectangle bounds + texture_offset = 17, + button_chars = util.make_ascii_button(250, '(') +} + +function Line:init() + self.options = { + thickness = { + name = "Line thickness", + type = "plusminus", + value = 1, + min = 1, + max = function(shape) if not shape.height or not shape.width then + return nil + else + return math.max(shape.height, shape.width) + end + end, + keys = { "CUSTOM_T", "CUSTOM_SHIFT_T" }, + }, + } end local function get_granularity(x0, y0, x1, y1, bezier_point1, bezier_point2) @@ -821,4 +820,4 @@ end -- module users can get shapes through this global, shape option values -- persist in these as long as the module is loaded -- idk enough lua to know if this is okay to do or not -all_shapes = { Rectangle {}, Ellipse {}, Rows {}, Diag {}, Line {}, FreeForm {}, Star {} } +all_shapes = { Rectangle {}, Ellipse {}, Star {}, Rows {}, Diag {}, Line {}, FreeForm {} } From bb553a014cf72fda011cd50980f2fa8c76ba2b19 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 27 Apr 2025 14:10:52 -0700 Subject: [PATCH 544/811] update moddable-gods --- changelog.txt | 1 + docs/modtools/moddable-gods.rst | 62 ++++++++--- modtools/moddable-gods.lua | 192 ++++++++++++++++++++------------ 3 files changed, 163 insertions(+), 92 deletions(-) diff --git a/changelog.txt b/changelog.txt index 7a74140c52..49a241e338 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: # Future ## New Tools +- `modtools/moddable-gods`: (reinstated) create new deities from scratch ## New Features - `gui/mod-manager`: when run in a loaded world, shows a list of active mods -- click to export the list to the clipboard for easy sharing or posting diff --git a/docs/modtools/moddable-gods.rst b/docs/modtools/moddable-gods.rst index 1a575c8325..22b890e6c5 100644 --- a/docs/modtools/moddable-gods.rst +++ b/docs/modtools/moddable-gods.rst @@ -3,21 +3,47 @@ modtools/moddable-gods .. dfhack-tool:: :summary: Create deities. - :tags: unavailable - -This is a standardized version of Putnam's moddableGods script. It allows you -to create gods on the command-line. - -Arguments:: - - -name godName - sets the name of the god to godName - if there's already a god of that name, the script halts - -spheres [ sphereList ] - define a space-separated list of spheres of influence of the god - -gender male|female|neuter - sets the gender of the god - -depictedAs str - often depicted as a str - -verbose - if specified, prints details about the created god + :tags: dev + +This script allows you to create new gods in an existing world. + +Usage +----- + +:: + + moddable-gods --name --spheres [] + +Examples +-------- + +``modtools/moddable-gods --name "Slarty Bog" --spheres FATE,WEATHER`` + Create a new god named "Slarty Bog" with spheres of influence of FATE and + WEATHER. The god will have a random gender and will be depicted as a dwarf. + +``modtools/moddable-gods -n Og -s SPEECH,SALT,SACRIFICE -g neuter -d emu`` + Create a new god named "Og" with spheres of influence of SPEECH, SALT, and + SACRIFICE. The god will be genderless and will be depicted as an emu. + +Options +------- + +``-n``, ``--name `` + The name of the god to create. This is a required argument. The name must + be unique in the world. If the name is already taken, the script will exit + without action. +``-s``, ``--spheres `` + A comma-separated list of spheres of influence for the god. This is a + required argument. To see the available spheres, run this command:: + + lua @df.sphere_type + +``-g``, ``--gender (male|female|neuter)`` + The gender of the god. If not specified, a random gender will be chosen. +``-d``, ``--depicted-as `` + When the deity is referenced in-game, it will be described as "often + depicted as a ". The string must match the token ID or descriptive + name of a race that exists in the world. You can also specify a numeric + race ID. If not specified, it defaults to "dwarf". +``-q``, ``--quiet`` + If specified, suppresses all non-error output. diff --git a/modtools/moddable-gods.lua b/modtools/moddable-gods.lua index 3d1726ba10..f852992faf 100644 --- a/modtools/moddable-gods.lua +++ b/modtools/moddable-gods.lua @@ -1,92 +1,136 @@ -local utils = require('utils') - -local validArgs = utils.invert{ - 'help', - 'name', - 'spheres', - 'gender', - 'depictedAs', - 'verbose', --- 'entities', -} -local args = utils.processArgs({...}, validArgs) +local argparse = require('argparse') + +local function get_spheres(arg) + local spheres = {} + for _, sphere in ipairs(argparse.stringList(arg, 'spheres')) do + local sphereType = df.sphere_type[sphere] + if not sphereType then + qerror('invalid sphere: ' .. sphere) + end + table.insert(spheres, sphereType) + end + return spheres +end -if args.help then - print(dfhack.script_help()) - return +local function get_gender(arg) + if arg == 'male' then + return df.pronoun_type.he + elseif arg == 'female' then + return df.pronoun_type.she + elseif arg == 'neuter' then + return df.pronoun_type.it + else + qerror('invalid gender: ' .. arg) + end end -if not args.name or not args.depictedAs or not args.spheres or not args.gender then - error('All arguments must be specified.') +local function get_race(arg) + local int_arg = tonumber(arg) + if int_arg then + local raw = df.creature_raw.find(int_arg) + if not raw then + qerror('race id ' .. int_arg .. ' does not exist') + end + return int_arg + end + for k, raw in ipairs(df.global.world.raws.creatures.all) do + if raw.creature_id == arg or raw.name[0] == arg then + return k + end + end + qerror('race ' .. arg .. ' does not exist') +end + +local function do_god(opts) + local godFig = df.historical_figure:new() + godFig.race = opts.race + godFig.caste = 0 + godFig.sex = opts.gender + + godFig.appeared_year = -1 + godFig.born_year = -1 + godFig.born_seconds = -1 + godFig.curse_year = -1 + godFig.curse_seconds = -1 + godFig.old_year = -1 + godFig.old_seconds = -1 + godFig.died_year = -1 + godFig.died_seconds = -1 + + godFig.name.has_name = true + godFig.name.first_name = opts.name + + godFig.breed_id = -1 + godFig.flags.deity = true + godFig.flags.brag_on_kill = true + godFig.flags.kill_quest = true + godFig.flags.chatworthy = true + godFig.flags.flashes = true + godFig.flags.never_cull = true + + godFig.info = df.historical_figure_info:new() + godFig.info.metaphysical = {new=true} + godFig.info.known_info = {new=true} + for _,sphere in ipairs(opts.spheres) do + godFig.info.metaphysical.spheres:insert('#', sphere) + end + + godFig.pool_id = -1 -- will get a pool_id when game is saved and reloaded + godFig.id = df.global.hist_figure_next_id + df.global.hist_figure_next_id = 1 + df.global.hist_figure_next_id + df.global.world.history.figures:insert('#', godFig) + + return godFig end -local templateGod -for _,fig in ipairs(df.global.world.history.figures) do - if fig.flags.deity then - templateGod = fig - break - end +if not dfhack.isWorldLoaded() then + qerror('This script requires a loaded world.') end -if not templateGod then - error 'Could not find template god.' + +local opts = { + name=nil, + spheres=nil, + gender=nil, + race=nil, + quiet=false, + help=false, +} + +local _ = argparse.processArgsGetopt({ ... }, { + {'n', 'name', hasArg=true, handler=function(arg) opts.name = arg end}, + {'s', 'spheres', hasArg=true, handler=function(arg) opts.spheres = get_spheres(arg) end}, + {'g', 'gender', hasArg=true, handler=function(arg) opts.gender = get_gender(arg) end}, + {'d', 'depicted-as', hasArg=true, handler=function(arg) opts.race = get_race(arg) end}, + {'h', 'help', handler=function() opts.help = true end}, + {'q', 'quiet', handler=function() opts.quiet = true end}, +}) + +if opts.help then + print(dfhack.script_help()) + return end -local gender -if args.gender == 'male' then - gender = 1 -elseif args.gender == 'female' then - gender = 0 -elseif args.gender == "neuter" then - gender = -1 -else - error 'invalid gender' +if not opts.name or not opts.spheres or #opts.name == 0 or #opts.spheres == 0 then + qerror('name and spheres must be specified.') end -local race -for k,v in ipairs(df.global.world.raws.creatures.all) do - if v.creature_id == args.depictedAs or v.name[0] == args.depictedAs then - race = k - break +for _, fig in ipairs(df.global.world.history.figures) do + if fig.name.first_name == opts.name then + print('god "' .. opts.name .. '" already exists.') + return end end -if not race then - error('invalid race: ' .. args.depictedAs) -end -for _,fig in ipairs(df.global.world.history.figures) do - if fig.name.first_name == args.name then - print('god ' .. args.name .. ' already exists. Skipping') - return - end +if not opts.gender then + opts.gender = math.random(-1, 1) end -local godFig = df.historical_figure:new() -godFig.appeared_year = -1 -godFig.born_year = -1 -godFig.born_seconds = -1 -godFig.curse_year = -1 -godFig.curse_seconds = -1 -godFig.old_year = -1 -godFig.old_seconds = -1 -godFig.died_year = -1 -godFig.died_seconds = -1 -godFig.name.has_name = true -godFig.breed_id = -1 -godFig.flags:assign(templateGod.flags) -godFig.id = df.global.hist_figure_next_id -df.global.hist_figure_next_id = 1+df.global.hist_figure_next_id -godFig.info = df.historical_figure_info:new() -godFig.info.spheres = {new=true} -godFig.info.known_info = df.knowledge_profilest:new() -godFig.race = race -godFig.caste = 0 -godFig.sex = gender -godFig.name.first_name = args.name -for _,sphere in ipairs(args.spheres) do - godFig.info.metaphysical.spheres:insert('#',df.sphere_type[sphere]) +if not opts.race then + opts.race = get_race('dwarf') end -df.global.world.history.figures:insert('#',godFig) -if args.verbose then - print(godFig.name.first_name .. " created as historical figure " .. tostring(godFig.id)) +local godFig = do_god(opts) + +if not opts.quiet then + print(godFig.name.first_name .. " created as historical figure " .. tostring(godFig.id)) end From 593275684e0ce4b05bfdb099aa7852c9f5b90116 Mon Sep 17 00:00:00 2001 From: Blake Walsh <5808762+blake-sc@users.noreply.github.com> Date: Mon, 28 Apr 2025 09:43:51 +0200 Subject: [PATCH 545/811] remove-stress also removes long-term stress, which also immediately removes stress and haggard statuses --- changelog.txt | 1 + remove-stress.lua | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/changelog.txt b/changelog.txt index 7a74140c52..a37db487f5 100644 --- a/changelog.txt +++ b/changelog.txt @@ -38,6 +38,7 @@ Template for new versions: - `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game ## Misc Improvements +- `remove-stress`: also applied to long-term stress, immediately removing stressed and haggard statuses ## Removed diff --git a/remove-stress.lua b/remove-stress.lua index 7cf0193645..40ce4946a3 100644 --- a/remove-stress.lua +++ b/remove-stress.lua @@ -1,6 +1,7 @@ -- Sets stress to negative one million --By Putnam; http://www.bay12forums.com/smf/index.php?topic=139553.msg5820486#msg5820486 --edited by Bumber +--edited by BlakeMW --@module = true local utils = require('utils') @@ -14,6 +15,9 @@ function removeStress(unit,value) if unit.status.current_soul.personality.stress > value then unit.status.current_soul.personality.stress = value end + if unit.status.current_soul.personality.longterm_stress > value then + unit.status.current_soul.personality.longterm_stress = value + end end end From 7e6c000698ef7f9e068c14cd92d844d2f2bba4e7 Mon Sep 17 00:00:00 2001 From: 83N170 <83N170@mail.com> Date: Mon, 28 Apr 2025 22:24:59 +0100 Subject: [PATCH 546/811] Reduced diameter of star as thickness increases to remain within bounding box --- changelog.txt | 8 +++++++- internal/design/shapes.lua | 24 ++++++++++++------------ 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/changelog.txt b/changelog.txt index 7a74140c52..17e016f769 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,10 +16,16 @@ Template for new versions: ## New Features -- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction +- `gui/design`: add new option to draw N-point stars. + - The default has 5 points, use 'B'/'b' to increase/decrease points. + - They can be hollow or filled, and inverted. + - The next-point offset can be increased/decreased using 'N'/'n' which particularly affects 7 point stars and above to make them spikier or smoother, but can also be used to decrease to 1 to make symmetrical polygons or increase to N which only paints the vertexes. + - The orientation can be changed by adding a main axis point using 'v' and moving this to point in the desired direction. ## Fixes +- `gui/design`: reduced diameter of star as thickness increases to remain within bounding box + ## Misc Improvements ## Removed diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index 34e8796c42..f3209fc196 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -411,12 +411,11 @@ function LineDrawer:plot_bresenham(x0, y0, x1, y1, thickness) local sy = y0 < y1 and 1 or -1 local e2, x, y - for i = 0, thickness - 1 do + for i = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do x = x0 y = y0 + i local err = dx - dy - local p = math.max(dx, dy) - while p >= 0 do + while true do for j = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do if not self.arr[x + j] then self.arr[x + j] = {} end if not self.arr[x + j][y] then @@ -440,7 +439,6 @@ function LineDrawer:plot_bresenham(x0, y0, x1, y1, thickness) err = err + dx y = y + sy end - p = p - 1 end end end @@ -770,11 +768,17 @@ function Star:update(points, extra_points) self.arr = {} if #points < self.min_points then return end self.threshold = self.options.total_points.value - 2 * self.options.next_point_offset.value + + local thickness = 1 + if self.options.hollow.value then + thickness = self.options.thickness.value + end + local top_left, bot_right = self:get_point_dims() - self.height = bot_right.y - top_left.y - self.width = bot_right.x - top_left.x - if self.height == 1 or self.width == 1 then return end - self.center = { x = self.width * 0.5, y = self.height * 0.5 } + self.height = bot_right.y - top_left.y - thickness + 1 + self.width = bot_right.x - top_left.x - thickness + 1 + if self.height < 2 or self.width < 2 then return end + self.center = { x = (bot_right.x - top_left.x + ((thickness - 1) % 2)) * 0.5, y = (bot_right.y - top_left.y + ((thickness - 1) % 2)) * 0.5 } local axes = {} axes[1] = (#extra_points > 0) and { x = extra_points[1].x - self.center.x - top_left.x, y = extra_points[1].y - self.center.y - top_left.y } or { x = 0, y = -self.center.y } @@ -786,10 +790,6 @@ function Star:update(points, extra_points) axes[a] = { x = math.cos(angle) * axes[1].x - math.sin(angle) * axes[1].y, y = math.sin(angle) * axes[1].x + math.cos(angle) * axes[1].y } end - local thickness = 1 - if self.options.hollow.value then - thickness = self.options.thickness.value - end self.lines = {} for l = 1, self.options.total_points.value do From cb1b108eaa2cf51006e3d65aae08ed50dd0fa898 Mon Sep 17 00:00:00 2001 From: 83N170 <83N170@mail.com> Date: Tue, 29 Apr 2025 21:41:31 +0100 Subject: [PATCH 547/811] Move details from changelog to docs --- changelog.txt | 8 +------- docs/gui/design.rst | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/changelog.txt b/changelog.txt index 17e016f769..7a74140c52 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,16 +16,10 @@ Template for new versions: ## New Features -- `gui/design`: add new option to draw N-point stars. - - The default has 5 points, use 'B'/'b' to increase/decrease points. - - They can be hollow or filled, and inverted. - - The next-point offset can be increased/decreased using 'N'/'n' which particularly affects 7 point stars and above to make them spikier or smoother, but can also be used to decrease to 1 to make symmetrical polygons or increase to N which only paints the vertexes. - - The orientation can be changed by adding a main axis point using 'v' and moving this to point in the desired direction. +- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction ## Fixes -- `gui/design`: reduced diameter of star as thickness increases to remain within bounding box - ## Misc Improvements ## Removed diff --git a/docs/gui/design.rst b/docs/gui/design.rst index 2c80170da1..2106c085e2 100644 --- a/docs/gui/design.rst +++ b/docs/gui/design.rst @@ -17,6 +17,45 @@ Usage gui/design +Shapes +------ + +- Rectangle + - They can be hollow or filled using 'h'. + - When hollow line thickness can be increased/decreased using 'T'/'t'. + - They can be inverted using 'i'. +- Ellipse + - They can be hollow or filled using 'h'. + - When hollow line thickness can be increased/decreased using 'T'/'t'. + - They can be inverted using 'i'. +- Star + - The default has 5 points, use 'B'/'b' to increase/decrease points. + - They can be hollow or filled using 'h'. + - When hollow line thickness can be increased/decreased using 'T'/'t'. + - They can be inverted using 'i'. + - The next-point offset can be increased/decreased using 'N'/'n' which + particularly affects 7 point stars and above to make them spikier or + smoother, but can also be used to decrease to 1 to make symmetrical + polygons or increase to N which only paints the vertexes. + - The orientation can be changed by adding a main axis point using 'v' and + moving this to point in the desired direction. +- Rows + - Vertical rows can be toggled using 'v'. + - Horizontal rows can be toggled using 'h'. + - Spacing can be increased/decreased using 'T'/'t'. + - They can be inverted using 'i'. +- Diagonal + - Direction can be reversed using 'r'. + - Spacing can be increased/decreased using 'T'/'t'. + - They can be inverted using 'i'. +- Line + - Line thickness can be increased/decreased using 'T'/'t'. + - Can be curved by adding one or more control points using 'v'. +- FreeForm + - Can be toggled open multi-line sequence or closed polygon using 'y' + - Line thickness can be increased/decreased using 'T'/'t'. + + Overlay ------- From cf488e38979085a1caa067b3b81c066d74295bd0 Mon Sep 17 00:00:00 2001 From: 83N170 <83N170@mail.com> Date: Tue, 29 Apr 2025 21:57:30 +0100 Subject: [PATCH 548/811] Make horizontal and vertical line thickness consistent --- internal/design/shapes.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index f3209fc196..c6d3c9debd 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -490,7 +490,7 @@ function Line:cubic_bezier(x0, y0, x1, y1, bezier_point1, bezier_point2, thickne 0.5) local y = math.floor(((1 - t) ^ 3 * y0 + 3 * (1 - t) ^ 2 * t * y2 + 3 * (1 - t) * t ^ 2 * y3 + t ^ 3 * y1) + 0.5) - for i = 0, thickness - 1 do + for i = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do for j = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do if not self.arr[x + j] then self.arr[x + j] = {} end if not self.arr[x + j][y + i] then @@ -507,7 +507,7 @@ function Line:cubic_bezier(x0, y0, x1, y1, bezier_point1, bezier_point2, thickne 0.5) local y_end = math.floor(((1 - 1) ^ 3 * y0 + 3 * (1 - 1) ^ 2 * 1 * y2 + 3 * (1 - 1) * 1 ^ 2 * y3 + 1 ^ 3 * y1) + 0.5) - for i = 0, thickness - 1 do + for i = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do for j = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do if not self.arr[x_end + j] then self.arr[x_end + j] = {} end if not self.arr[x_end + j][y_end + i] then From 435fcd066dcd6ee0e271a67fbd2e4d1dbfec376c Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 23 Mar 2025 05:35:01 -0500 Subject: [PATCH 549/811] confirm: only show pause option for pausable confirmations Prevent dialogs.showYesNoPrompt from showing the pause option by passing a nil on_pause argument when a confirmation is not pausable (e.g., trade-cancel, depot-remove). --- changelog.txt | 1 + confirm.lua | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 49a241e338..c9b0c8ac22 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,6 +37,7 @@ Template for new versions: ## Fixes - `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session - `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game +- `confirm`: only show pause option for pausable confirmations ## Misc Improvements diff --git a/confirm.lua b/confirm.lua index fb0a108ed6..9fb114e9b3 100644 --- a/confirm.lua +++ b/confirm.lua @@ -131,8 +131,9 @@ function ConfirmOverlay:onInput(keys) gui.simulateInput(scr, keys) self.simulating = false end + local pause_fn = conf.pausable and curry(propagate_fn, true) or nil dialogs.showYesNoPrompt(conf.title, utils.getval(conf.message):wrap(45), COLOR_YELLOW, - propagate_fn, nil, curry(propagate_fn, true), curry(dfhack.run_script, 'gui/confirm', tostring(conf.id))) + propagate_fn, nil, pause_fn, curry(dfhack.run_script, 'gui/confirm', tostring(conf.id))) return true end end From 2a73f93541404ab181abaf06fb9f392c02c65a7a Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 23 Mar 2025 05:58:55 -0500 Subject: [PATCH 550/811] confirm: handle LEAVESCREEN in uniform-discard-changes Match the _MOUSE_R handling: prompt if there are uniform changes. --- changelog.txt | 1 + internal/confirm/specs.lua | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index c9b0c8ac22..ad4e2676d7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -38,6 +38,7 @@ Template for new versions: - `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session - `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game - `confirm`: only show pause option for pausable confirmations +- `confirm`: when editing a uniform, confirm discard of changes when exiting with Escape ## Misc Improvements diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 73b9179e41..03d55fae82 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -325,13 +325,13 @@ ConfirmSpec{ id='uniform-discard-changes', title='Discard uniform changes', message='Are you sure you want to discard changes to this uniform?', - intercept_keys={'_MOUSE_L', '_MOUSE_R'}, + intercept_keys={'LEAVESCREEN', '_MOUSE_L', '_MOUSE_R'}, -- sticks out the left side so it can move with the panel -- when the screen is resized too narrow intercept_frame={r=32, t=19, w=101, b=3}, context='dwarfmode/Squads/Equipment/Customizing/Default', predicate=function(keys, mouse_offset) - if keys._MOUSE_R then + if keys.LEAVESCREEN or keys._MOUSE_R then return uniform_has_changes() end if clicked_on_confirm_button(mouse_offset) then From fe09c126e4811ab43a5e7e1eaf1b2a954d3c11a8 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Wed, 9 Apr 2025 06:52:09 -0500 Subject: [PATCH 551/811] confirm: use interface rect for order-remove calculations When the DF interface percentage is not 100, the interface width can be smaller than the window width. For certain sizes (depending on the interface percentage), the following condition can hold: - interface width <= 154 < window width In this situation, the info window tab row will not have "unwrapped" (from four to two UI rows), but the previous order index calculation code would assume that it had (due to using the window width). This two UI row discrepancy caused the calculated order index to be one too high (and possibly out of bounds) when clicking on either of the bottom two UI rows of an order remove button. The incorrect index caused the confirmation to display the description of the following order. --- changelog.txt | 1 + internal/confirm/specs.lua | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index ad4e2676d7..02bd55ccd1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -39,6 +39,7 @@ Template for new versions: - `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game - `confirm`: only show pause option for pausable confirmations - `confirm`: when editing a uniform, confirm discard of changes when exiting with Escape +- `confirm`: when removing a manager order, show correct order description when using non-100% interface setting ## Misc Improvements diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 03d55fae82..9147674330 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -7,6 +7,7 @@ local json = require('json') local trade_internal = reqscript('internal/caravan/trade') +local gui = require('gui') local CONFIG_FILE = 'dfhack-config/confirm.json' @@ -457,7 +458,7 @@ ConfirmSpec{ message=function() local order_desc = '' local scroll_pos = mi.info.work_orders.scroll_position_work_orders - local y_offset = dfhack.screen.getWindowSize() > 154 and 8 or 10 + local y_offset = gui.get_interface_rect().width > 154 and 8 or 10 local _, y = dfhack.screen.getMousePos() if y then local order_idx = scroll_pos + (y - y_offset) // 3 From ded125a055f0d7b452b7628156ca88ce68c44f03 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Mon, 24 Mar 2025 02:38:37 -0500 Subject: [PATCH 552/811] confirm: order-remove: work around stale scroll position The scroll_position_work_orders value is being used here as the index of the first displayed order. DF seems to maintain this when the list view is updated via scrolling, but DF does not update it in at least two important situations: - when an order is removed, and - when the order list view grows enough to display additional orders (e.g., by increasing the height of the DF window). When the order list view is not scrolled all the way to the bottom, DF handles these actions by pushing orders into view at the bottom of the list view. Since the same order is still at the top of the list view, this does not require an update of the reported scroll position value. However, when the order list view is scrolled all the way to the bottom, DF handles these actions by pushing orders into view at the *top* of the list view. Since a new order is now at the top of the list view, we would expect that DF should have updated the reported scroll position, but it does not. The lack of scroll position update breaks our expectation that the reported scroll position should match the index of the first displayed order. If N orders have been removed and M order rows have been added to the order list view (after having scrolled to the bottom of the order list), our calculated order_idx will be N+M too high (causing mismatched descriptions in the confirmation dialogs, and out-of-bounds errors that entirely prevent confirmation when acting on the last N+M orders!). When there are at least as many orders as the height of the orders list view, DF seems to always keep the bottom of the list view populated. Assume this is will be case and adjust our order_idx calculation when the reported scroll position would put the effective index of the last order out of bounds. If DF's order list view ever changes to displaying empty order rows even when there are orders that could be "pushed in" from the top, this will need to be revisited. --- changelog.txt | 1 + internal/confirm/specs.lua | 11 ++++++++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 02bd55ccd1..d373953650 100644 --- a/changelog.txt +++ b/changelog.txt @@ -40,6 +40,7 @@ Template for new versions: - `confirm`: only show pause option for pausable confirmations - `confirm`: when editing a uniform, confirm discard of changes when exiting with Escape - `confirm`: when removing a manager order, show correct order description when using non-100% interface setting +- `confirm`: when removing a manager order, show correct order description after prior order removal or window resize (when scrolled to bottom of order list) ## Misc Improvements diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 9147674330..4526daa6bb 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -458,7 +458,16 @@ ConfirmSpec{ message=function() local order_desc = '' local scroll_pos = mi.info.work_orders.scroll_position_work_orders - local y_offset = gui.get_interface_rect().width > 154 and 8 or 10 + local ir = gui.get_interface_rect() + local y_offset = ir.width > 154 and 8 or 10 + local order_rows = (ir.height - y_offset - 9) // 3 + local max_scroll_pos = math.max(0, #orders - order_rows) -- DF keeps list view "full" (no empty rows at bottom), if possible + if scroll_pos > max_scroll_pos then + -- sometimes, DF does not adjust scroll_position_work_orders (when + -- scrolled to bottom: order removed, or list view height grew); + -- compensate to keep order_idx in sync (and in bounds) + scroll_pos = max_scroll_pos + end local _, y = dfhack.screen.getMousePos() if y then local order_idx = scroll_pos + (y - y_offset) // 3 From fc3e30038ffd16d5a9c6a67b30ae57371cfdb607 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 25 Mar 2025 05:36:01 -0500 Subject: [PATCH 553/811] confirm: rework order-remove description generation Handle possibly out-of-bounds order index. Factor out call to material description generation. --- internal/confirm/specs.lua | 52 +++++++++++++++++++++++--------------- 1 file changed, 32 insertions(+), 20 deletions(-) diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 4526daa6bb..5abd2aa58a 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -431,7 +431,7 @@ ConfirmSpec{ end, } -local function make_order_desc(order, noun) +local function make_order_material_desc(order, noun) local desc = '' if order.mat_type >= 0 then local matinfo = dfhack.matinfo.decode(order.mat_type, order.mat_index) @@ -452,6 +452,34 @@ end local orders = df.global.world.manager_orders.all local itemdefs = df.global.world.raws.itemdefs local reactions = df.global.world.raws.reactions.reactions + +local function make_order_desc(order) + if order.job_type == df.job_type.CustomReaction then + for _, reaction in ipairs(reactions) do + if reaction.code == order.reaction_name then + return reaction.name + end + end + return '' + end + local noun + if order.job_type == df.job_type.MakeArmor then + noun = itemdefs.armor[order.item_subtype].name + elseif order.job_type == df.job_type.MakeWeapon then + noun = itemdefs.weapons[order.item_subtype].name + elseif order.job_type == df.job_type.MakePants then + noun = itemdefs.pants[order.item_subtype].name + elseif order.job_type == df.job_type.MakeTool then + noun = itemdefs.tools[order.item_subtype].name + elseif order.job_type == df.job_type.SmeltOre then + noun = 'ore' + else + -- caption is usually "verb noun(-phrase)" + noun = df.job_type.attrs[order.job_type].caption + end + return make_order_material_desc(order, noun) +end + ConfirmSpec{ id='order-remove', title='Remove manger order', @@ -471,25 +499,9 @@ ConfirmSpec{ local _, y = dfhack.screen.getMousePos() if y then local order_idx = scroll_pos + (y - y_offset) // 3 - local order = orders[order_idx] - if order.job_type == df.job_type.CustomReaction then - for _, reaction in ipairs(reactions) do - if reaction.code == order.reaction_name then - order_desc = reaction.name - end - end - elseif order.job_type == df.job_type.MakeArmor then - order_desc = make_order_desc(order, itemdefs.armor[order.item_subtype].name) - elseif order.job_type == df.job_type.MakeWeapon then - order_desc = make_order_desc(order, itemdefs.weapons[order.item_subtype].name) - elseif order.job_type == df.job_type.MakePants then - order_desc = make_order_desc(order, itemdefs.pants[order.item_subtype].name) - elseif order.job_type == df.job_type.SmeltOre then - order_desc = make_order_desc(order, 'ore') - elseif order.job_type == df.job_type.MakeTool then - order_desc = make_order_desc(order, itemdefs.tools[order.item_subtype].name) - else - order_desc = make_order_desc(order, df.job_type.attrs[order.job_type].caption) + local order = safe_index(orders, order_idx) + if order then + order_desc = make_order_desc(order) end end return ('Are you sure you want to remove this manager order?\n\n%s'):format(dfhack.capitalizeStringWords(order_desc)) From 4bce233fb8f11664c7fb5f69d4ba571e6a90b9d1 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Tue, 25 Mar 2025 05:44:44 -0500 Subject: [PATCH 554/811] confirm: more specific order-remove descriptions Let order removal confirmation show the specific variety of - shield (shield, buckler), and - helm (helm, cap, hood), - gloves (gauntlets, gloves, mittens), - shoes (shoes, high boots, low boots, socks), - ammo (bolt), - trap component (axe blade, corkscrew, ball, disc, spike), and - meal (easy, fine, lavish). Most of these were previously described using the "generic" variety ("Gloves" for gauntlets, "Shoes" for socks, etc.), but meals looked particularly odd since they were previously described as "Coral", "Green Glass", and "Clear Glass" "Prepare Meal". `itemdefs.food` is not used because those list the final item names (biscuits, stew, roast), not the meal "type" (easy, fine, lavish). --- changelog.txt | 1 + internal/confirm/specs.lua | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/changelog.txt b/changelog.txt index d373953650..603dac8b5d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -41,6 +41,7 @@ Template for new versions: - `confirm`: when editing a uniform, confirm discard of changes when exiting with Escape - `confirm`: when removing a manager order, show correct order description when using non-100% interface setting - `confirm`: when removing a manager order, show correct order description after prior order removal or window resize (when scrolled to bottom of order list) +- `confirm`: when removing a manager order, show specific item/job type for ammo, shield, helm, gloves, shoes, trap component, and meal orders ## Misc Improvements diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 5abd2aa58a..8daffa92d4 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -453,6 +453,12 @@ local orders = df.global.world.manager_orders.all local itemdefs = df.global.world.raws.itemdefs local reactions = df.global.world.raws.reactions.reactions +local meal_type_by_ingredient_count = { + [2] = 'easy', + [3] = 'fine', + [4] = 'lavish', +} + local function make_order_desc(order) if order.job_type == df.job_type.CustomReaction then for _, reaction in ipairs(reactions) do @@ -461,16 +467,35 @@ local function make_order_desc(order) end end return '' + elseif order.job_type == df.job_type.PrepareMeal then + -- DF uses mat_type as ingredient count? + local meal_type = meal_type_by_ingredient_count[order.mat_type] + if meal_type then + return 'prepare ' .. meal_type .. ' meal' + end + return 'prepare meal' end local noun if order.job_type == df.job_type.MakeArmor then noun = itemdefs.armor[order.item_subtype].name elseif order.job_type == df.job_type.MakeWeapon then noun = itemdefs.weapons[order.item_subtype].name + elseif order.job_type == df.job_type.MakeShield then + noun = itemdefs.shields[order.item_subtype].name + elseif order.job_type == df.job_type.MakeAmmo then + noun = itemdefs.ammo[order.item_subtype].name + elseif order.job_type == df.job_type.MakeHelm then + noun = itemdefs.helms[order.item_subtype].name + elseif order.job_type == df.job_type.MakeGloves then + noun = itemdefs.gloves[order.item_subtype].name elseif order.job_type == df.job_type.MakePants then noun = itemdefs.pants[order.item_subtype].name + elseif order.job_type == df.job_type.MakeShoes then + noun = itemdefs.shoes[order.item_subtype].name elseif order.job_type == df.job_type.MakeTool then noun = itemdefs.tools[order.item_subtype].name + elseif order.job_type == df.job_type.MakeTrapComponent then + noun = itemdefs.trapcomps[order.item_subtype].name elseif order.job_type == df.job_type.SmeltOre then noun = 'ore' else From fb25d4d2e7d0b34cbdbb9c1f175573729cb5ae02 Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sun, 23 Mar 2025 06:13:55 -0500 Subject: [PATCH 555/811] confirm: only pause specific confirmations Pausing a confirmation currently pauses all confirmations, not just other instances of the current confirmation. For example, when trading, pausing a Mark All confirmation will also skip confirmation of the Seize action. The prompt in showYesNoPrompt is "Pause this confirmation". To better match that description, only pause new occurrences of the current confirmation. --- changelog.txt | 1 + confirm.lua | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 603dac8b5d..6b3d279ae1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -42,6 +42,7 @@ Template for new versions: - `confirm`: when removing a manager order, show correct order description when using non-100% interface setting - `confirm`: when removing a manager order, show correct order description after prior order removal or window resize (when scrolled to bottom of order list) - `confirm`: when removing a manager order, show specific item/job type for ammo, shield, helm, gloves, shoes, trap component, and meal orders +- `confirm`: the pause option now only pauses future instances of the current confirmation instead of all confirmations in the current context ## Misc Improvements diff --git a/confirm.lua b/confirm.lua index 9fb114e9b3..5d1944d29b 100644 --- a/confirm.lua +++ b/confirm.lua @@ -108,12 +108,15 @@ function ConfirmOverlay:matches_conf(conf, keys, scr) end function ConfirmOverlay:onInput(keys) - if self.paused_conf or self.simulating then + if self.simulating then return false end local scr = dfhack.gui.getDFViewscreen(true) for id, conf in pairs(specs.REGISTRY) do if specs.config.data[id].enabled and self:matches_conf(conf, keys, scr) then + if conf == self.paused_conf then + return false + end local mouse_pos = xy2pos(dfhack.screen.getMousePos()) local propagate_fn = function(pause) if conf.on_propagate then From f22f4bd63ac133da4d166efadf45b3f38928c648 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 3 May 2025 10:36:03 -0700 Subject: [PATCH 556/811] rewrite and reintroduce deteriorate --- changelog.txt | 1 + deteriorate.lua | 486 ++++++++++++++++------------ docs/deteriorate.rst | 111 +++---- internal/control-panel/registry.lua | 7 + 4 files changed, 344 insertions(+), 261 deletions(-) diff --git a/changelog.txt b/changelog.txt index 49a241e338..d5f006fd7a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: # Future ## New Tools +- `deteriorate`: (reinstated) allow corpses, body parts, food, and/or damaged clothes to rot away - `modtools/moddable-gods`: (reinstated) create new deities from scratch ## New Features diff --git a/deteriorate.lua b/deteriorate.lua index ddd6c0f872..2d6aa0164a 100644 --- a/deteriorate.lua +++ b/deteriorate.lua @@ -1,88 +1,142 @@ -- Cause selected item types to quickly rot away --@module = true +--@enable = true local argparse = require('argparse') local utils = require('utils') -local function get_clothes_vectors() - return {df.global.world.items.other.GLOVES, - df.global.world.items.other.ARMOR, - df.global.world.items.other.SHOES, - df.global.world.items.other.PANTS, - df.global.world.items.other.HELM} +-------------------- +-- state + +local GLOBAL_KEY = 'deteriorate' + +local categories = { + 'clothes', + 'food', + 'corpses', + 'usable-parts', + 'unusable-parts', +} + +local aliases = { + parts={'usable-parts', 'unusable-parts'}, + all=categories, +} + +local function get_default_state() + local default_state = { + enabled=false, + categories={}, + } + for _,category in ipairs(categories) do + local default_enabled = category == 'corpses' or category == 'unusable-parts' + default_state.categories[category] = { + enabled=default_enabled, + frequency=1, + last_cycle_tick=0, + } + end + return default_state end -local function get_corpse_vectors() - return {df.global.world.items.other.ANY_CORPSE} +state = state or get_default_state() + +function isEnabled() + return state.enabled end -local function get_remains_vectors() - return {df.global.world.items.other.REMAINS} +local function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, state) +end + +----------------------- +-- deterioration logic + +local function get_clothes_vectors() + return { + df.global.world.items.other.GLOVES, + df.global.world.items.other.ARMOR, + df.global.world.items.other.SHOES, + df.global.world.items.other.PANTS, + df.global.world.items.other.HELM, + } end local function get_food_vectors() - return {df.global.world.items.other.FISH, - df.global.world.items.other.FISH_RAW, - df.global.world.items.other.EGG, - df.global.world.items.other.CHEESE, - df.global.world.items.other.PLANT, - df.global.world.items.other.PLANT_GROWTH, - df.global.world.items.other.FOOD} + return { + df.global.world.items.other.FISH, + df.global.world.items.other.FISH_RAW, + df.global.world.items.other.EGG, + df.global.world.items.other.CHEESE, + df.global.world.items.other.PLANT, + df.global.world.items.other.PLANT_GROWTH, + df.global.world.items.other.FOOD, + df.global.world.items.other.MEAT, + df.global.world.items.other.LIQUID_MISC, + } +end + +local function get_corpse_vectors() + return { + df.global.world.items.other.CORPSE, + df.global.world.items.other.REMAINS, + } +end + +local function get_parts_vectors() + return { + df.global.world.items.other.CORPSEPIECE, + } end local function is_valid_clothing(item) + -- includes discarded owned clothes return item.subtype.armorlevel == 0 and item.flags.on_ground and item.wear > 0 end -local function keep_usable(opts, item) - return opts.keep_usable and ( - not item.corpse_flags.unbutchered and ( - item.corpse_flags.bone or - item.corpse_flags.horn or - item.corpse_flags.leather or - item.corpse_flags.skull or - item.corpse_flags.tooth) or ( - item.corpse_flags.hair_wool or - item.corpse_flags.pearl or - item.corpse_flags.plant or - item.corpse_flags.shell or - item.corpse_flags.silk or - item.corpse_flags.yarn) ) +local function is_valid_food(item) + if not df.item_liquid_miscst:is_instance(item) then + return true + end + local mi = dfhack.matinfo.decode(item) + return mi:getToken():endswith(':MILK') end -local function is_valid_corpse(opts, item) - -- check if the corpse is a resident of the fortress and is not keep_usable - local unit = df.unit.find(item.unit_id) - if not unit then - return not keep_usable(opts, item) - end - local hf = df.historical_figure.find(unit.hist_figure_id) - if not hf then - return not keep_usable(opts, item) - end - for _,link in ipairs(hf.entity_links) do - if link.entity_id == df.global.plotinfo.group_id and df.histfig_entity_link_type[link:getType()] == 'MEMBER' then - return false - end - end - return not keep_usable(opts, item) +-- TODO: is just checking in_building sufficient, or do we need to validate +-- that the building it is in is a coffin? +local function is_entombed(item) + return item.flags.in_building +end + +local function is_valid_corpse(item) + return not is_entombed(item) +end + +local function is_usable_corpse_piece(item) + return item.corpse_flags.hair_wool or + item.corpse_flags.pearl or + item.corpse_flags.plant or + item.corpse_flags.shell or + item.corpse_flags.silk or + item.corpse_flags.yarn end -local function is_valid_remains(opts, item) - return true +local function is_valid_usable_corpse_piece(item) + return not is_entombed(item) and is_usable_corpse_piece(item) end -local function is_valid_food(opts, item) - return true +local function is_valid_unusable_corpse_piece(item) + return not is_entombed(item) and not is_usable_corpse_piece(item) end +-- different algorithm for clothes so they rot away when they become tattered local function increment_clothes_wear(item) item.wear_timer = math.ceil(item.wear_timer * (item.wear + 0.5)) return item.wear > 2 end -local function increment_generic_wear(item, threshold) +local function increment_wear(threshold, item) item.wear_timer = item.wear_timer + 1 if item.wear_timer > threshold then item.wear_timer = 0 @@ -91,211 +145,229 @@ local function increment_generic_wear(item, threshold) return item.wear > 3 end -local function increment_corpse_wear(item) - return increment_generic_wear(item, 24) -end - -local function increment_remains_wear(item) - return increment_generic_wear(item, 6) -end - -local function increment_food_wear(item) - return increment_generic_wear(item, 24) -end - -local function deteriorate(opts, get_item_vectors_fn, is_valid_fn, increment_wear_fn) - local count = 0 +local function deteriorate_items(now, get_item_vectors_fn, is_valid_fn, increment_wear_fn) + local items_to_remove = {} for _,v in ipairs(get_item_vectors_fn()) do for _,item in ipairs(v) do - if is_valid_fn(opts, item) and increment_wear_fn(item) - and not item.flags.garbage_collect then - dfhack.items.remove(item) - count = count + 1 + if is_valid_fn(item) and (now or increment_wear_fn(item)) and not item.flags.garbage_collect then + table.insert(items_to_remove, item) end end end - return count + for _,item in ipairs(items_to_remove) do + print(('deteriorate: %s crumbles away to dust'):format(dfhack.items.getReadableDescription(item))) + dfhack.items.remove(item) + end + return #items_to_remove end -local function always_worn() - return true +local function mk_deteriorate_fn(get_item_vectors_fn, is_valid_fn, increment_wear_fn) + return function(now) + return deteriorate_items(now, get_item_vectors_fn, is_valid_fn, increment_wear_fn) + end end -local function deteriorate_clothes(opts, now) - return deteriorate(opts, get_clothes_vectors, is_valid_clothing, - now and always_worn or increment_clothes_wear) -end +local category_fns = { + clothes=mk_deteriorate_fn(get_clothes_vectors, is_valid_clothing, increment_clothes_wear), + food=mk_deteriorate_fn(get_food_vectors, is_valid_food, curry(increment_wear, 24)), + corpses=mk_deteriorate_fn(get_corpse_vectors, is_valid_corpse, curry(increment_wear, 24)), + ['usable-parts']=mk_deteriorate_fn(get_parts_vectors, is_valid_usable_corpse_piece, curry(increment_wear, 24)), + ['unusable-parts']=mk_deteriorate_fn(get_parts_vectors, is_valid_unusable_corpse_piece, curry(increment_wear, 24)), +} -local function deteriorate_corpses(opts, now) - return deteriorate(opts, get_corpse_vectors, is_valid_corpse, - now and always_worn or increment_corpse_wear) - + deteriorate(opts, get_remains_vectors, is_valid_remains, - now and always_worn or increment_remains_wear) -end +---------------------------- +-- cycle and timer logic -local function deteriorate_food(opts, now) - return deteriorate(opts, get_food_vectors, is_valid_food, - now and always_worn or increment_food_wear) +local TICKS_PER_DAY = 1200 +local TICKS_PER_MONTH = 28 * TICKS_PER_DAY +local TICKS_PER_YEAR = 12 * TICKS_PER_MONTH + +local function get_normalized_tick() + return dfhack.world.ReadCurrentTick() + TICKS_PER_YEAR * dfhack.world.ReadCurrentYear() end -local type_fns = { - clothes=deteriorate_clothes, - corpses=deteriorate_corpses, - food=deteriorate_food, -} +timeout_ids = timeout_ids or {} --- maps the type string to {id=int, time=int, timeunit=string} -timeout_ids = timeout_ids or { - clothes={}, - corpses={}, - food={}, -} +local function event_loop(category) + local category_data = state.categories[category] + if not state.enabled or not category_data.enabled then return end -local function _stop(item_type) - local timeout_id = timeout_ids[item_type].id - if timeout_id then - dfhack.timeout_active(timeout_id, nil) -- cancel callback - timeout_ids[item_type].id = nil - return true + local current_tick = get_normalized_tick() + local ticks_per_cycle = math.max(1, math.floor(TICKS_PER_DAY * category_data.frequency)) + local timeout_ticks = ticks_per_cycle + + if current_tick - category_data.last_cycle_tick < ticks_per_cycle then + timeout_ticks = category_data.last_cycle_tick - current_tick + ticks_per_cycle + else + category_fns[category](false) + category_data.last_cycle_tick = current_tick + persist_state() end + timeout_ids[category] = dfhack.timeout(timeout_ticks, 'ticks', curry(event_loop, category)) end -local function make_timeout_cb(item_type, opts) - local fn - fn = function(first_time) - local timeout_data = timeout_ids[item_type] - timeout_data.time, timeout_data.mode = opts.time, opts.mode - timeout_data.id = dfhack.timeout(opts.time, opts.mode, fn) - if not timeout_ids[item_type].id then - print('Map has been unloaded; stopping deteriorate') - for k in pairs(type_fns) do - _stop(k) - end - return - end - if not first_time then - local count = type_fns[item_type](opts) - if count > 0 then - print(('Deteriorated %d %s'):format(count, item_type)) - end - end - end - return fn +-- launches timer. first cycle will be after the configured frequency +local function start_category(category, category_data, current_tick) + category_data = category_data or state.categories[category] + category_data.last_cycle_tick = current_tick or get_normalized_tick() + event_loop(category) end -local function start(opts) - for _,v in ipairs(opts.types) do - _stop(v) - if not opts.quiet then - print(('Deterioration of %s commencing...'):format(v)) - end - -- create a callback and call it to make it register itself - make_timeout_cb(v, opts)(true) +local function stop_category(category) + local timeout_id = timeout_ids[category] + if timeout_id then + dfhack.timeout_active(timeout_id, nil) -- cancel callback + timeout_ids[category] = nil end end -local function stop(opts) - for _,v in ipairs(opts.types) do - if _stop(v) and not opts.quiet then - print('Stopped deteriorating ' .. v) +local function do_enable() + if state.enabled then return end + + state.enabled = true + local current_tick = get_normalized_tick() + for _,category in ipairs(categories) do + local category_data = state.categories[category] + if category_data.enabled then + start_category(category, category_data, current_tick) end end end -local function status() - for k in pairs(type_fns) do - local timeout_data = timeout_ids[k] - local status_str = 'Stopped' - if timeout_data.id then - local time, mode = timeout_data.time, timeout_data.mode - if time == 1 then - mode = mode:sub(1, #mode - 1) -- make singular - end - status_str = ('Running (every %s %s)') :format(time, mode) - end - print(('%7s:\t%s'):format(k, status_str)) +local function do_disable() + if not state.enabled then return end + + state.enabled = false + for _,category in ipairs(categories) do + stop_category(category) end end -local function now(opts) - for _,v in ipairs(opts.types) do - local count = type_fns[v](opts, true) - if not opts.quiet then - print(('Deteriorated %d %s'):format(count, v)) - end +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + do_disable() + return end -end -local function help() - print(dfhack.script_help()) + if sc ~= SC_MAP_LOADED or not dfhack.world.isFortressMode() then + return + end + + state = get_default_state() + utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) + + event_loop() end +--------------------- +-- CLI + if dfhack_flags.module then return end -if not dfhack.isMapLoaded() then - qerror('deteriorate needs a fortress map to be loaded.') +if dfhack_flags.enable then + if dfhack_flags.enable_state then + do_enable() + else + do_disable() + end end -local command_switch = { - start=start, - stop=stop, - status=status, - now=now, -} - -local valid_timeunits = utils.invert{'days', 'months', 'years'} - -local function parse_freq(arg) - local elems = argparse.stringList(arg) - local num = tonumber(elems[1]) - if not num or num <= 0 then - qerror('number parameter for --freq option must be greater than 0') +local function parse_categories(arg) + local list = {} + for _,v in ipairs(argparse.stringList(arg)) do + if aliases[v] then + for _,alias in ipairs(aliases[v]) do + table.insert(list, alias) + end + elseif category_fns[v] then + table.insert(list, v) + else + qerror(('unrecognized category: "%s"'):format(v)) + end end - if #elems == 1 then - return num, 'days' + if #list == 0 then + qerror('no categories specified') end - local timeunit = elems[2]:lower() - if valid_timeunits[timeunit] then return num, timeunit end - timeunit = timeunit .. 's' -- it's ok if the user specified a singular - if valid_timeunits[timeunit] then return num, timeunit end - qerror(('invalid time unit: "%s"'):format(elems[2])) + return list end -local function parse_types(arg) - local types = argparse.stringList(arg) - for _,v in ipairs(types) do - if not type_fns[v] then - qerror(('unrecognized type: "%s"'):format(v)) +local function status() + local running_str = state.enabled and 'Running' or 'Would run' + print(('deteriorate is %s'):format(state.enabled and 'enabled' or 'disabled')) + print() + for _,category in pairs(categories) do + local status_str = 'Stopped' + local category_data = state.categories[category] + if category_data.enabled then + status_str = ('%s every %s day%s') :format(running_str, + category_data.frequency, category_data.frequency == 1 and '' or 's') end + print(('%18s: %s'):format(category, status_str)) end - return types end -local opts = { - time = 1, - mode = 'days', - quiet = false, - types = {}, - keep_usable = false, - help = false, -} +local help = false -local nonoptions = argparse.processArgsGetopt({...}, { - {'f', 'freq', 'frequency', hasArg=true, - handler=function(optarg) opts.time,opts.mode = parse_freq(optarg) end}, - {'h', 'help', handler=function() opts.help = true end}, - {'q', 'quiet', handler=function() opts.quiet = true end}, - {'k', 'keep-usable', handler=function() opts.keep_usable = true end}, - {'t', 'types', hasArg=true, - handler=function(optarg) opts.types = parse_types(optarg) end}}) +local positionals = argparse.processArgsGetopt({...}, { + {'h', 'help', handler=function() help = true end}, +}) -local command = nonoptions[1] -if not command or not command_switch[command] then opts.help = true end +local command = table.remove(positionals, 1) +if command == 'help' or help then + print(dfhack.script_help()) + return +end -if not opts.help and command ~= 'status' and #opts.types == 0 then - qerror('no item types specified! try adding a --types parameter.') +if not command or command == 'status' then + status() +elseif command == 'enable' then + local cats = parse_categories(positionals[1]) + for _,v in ipairs(cats) do + if state.categories[v].enabled then + goto continue + end + state.categories[v].enabled = true + if state.enabled then + start_category(v) + end + ::continue:: + end +elseif command == 'disable' then + local cats = parse_categories(positionals[1]) + for _,v in ipairs(cats) do + if not state.categories[v].enabled then + goto continue + end + state.categories[v].enabled = false + if state.enabled then + stop_category(v) + end + ::continue:: + end +elseif command == 'frequency' or command == 'freq' then + local freq = tonumber(positionals[1]) + if not freq or freq <= 0 then + qerror('frequency must be greater than 0') + end + local cats = parse_categories(positionals[2]) + for _,v in ipairs(cats) do + state.categories[v].frequency = freq + if state.enabled then + stop_category(v) + start_category(v) + end + end +elseif command == 'now' then + local cats = parse_categories(positionals[1]) + local count = 0 + for _,v in ipairs(cats) do + count = count + category_fns[v](true) + end + print(('Deteriorated %d item%s'):format(count, count == 1 and '' or 's')) +else + qerror('unrecognized command: "' .. command .. '"') end -(command_switch[command] or help)(opts) +persist_state() diff --git a/docs/deteriorate.rst b/docs/deteriorate.rst index 583150afdb..f97cc3b089 100644 --- a/docs/deteriorate.rst +++ b/docs/deteriorate.rst @@ -3,11 +3,12 @@ deteriorate .. dfhack-tool:: :summary: Cause corpses, clothes, and/or food to rot away over time. - :tags: unavailable + :tags: fort fps gameplay items When enabled, this script will cause the specified item types to slowly rot -away. By default, items disappear after a few months, but you can choose to slow -this down or even make things rot away instantly! +away. As they deteriorate, they will acquire the normal ``x``, ``X``, and +``XX`` markings. By default, items disappear after a few months, but you can +choose to slow this down or even make things rot away instantly! Now all those slightly worn wool shoes that dwarves scatter all over the place or the toes, teeth, fingers, and limbs from the last undead siege will @@ -15,61 +16,63 @@ deteriorate at a greatly increased rate, and eventually just crumble into nothing. As warm and fuzzy as a dining room full of used socks makes your dwarves feel, your FPS does not like it! +By default (if you run ``enable deteriorate`` without changing any settings), +only non-entombed corpses and non-usable body parts will be affected. + +You can set other common options for new forts on the Gameplay / Autostart tab +of the DFHack control panel. + Usage ----- -``deteriorate start --types [--freq ] [--quiet] [--keep-usable]`` - Starts deteriorating the specified item types while you play. -``deteriorate stop --types `` - Stops deteriorating the specified item types. -``deteriorate status`` - Shows the item types that are currently being monitored and their - deterioration frequencies. -``deteriorate now --types [--quiet] [--keep-usable]`` - Causes all items (of the specified item types) to rot away within a few - ticks. - -You can have different types of items rotting away at different rates by running -``deteriorate start`` multiple times with different options. +:: + + enable deteriorate + deteriorate [status] + deteriorate (enable|disable) + deteriorate frequency + deteriorate now + +Where ```` is a comma-separated list of item types to affect. The +following categories are available: + +:clothes: All non-armor clothing pieces that are lying on the ground + that already have some damage. +:food: All food and plants. Milk is included, but seeds are left + untouched. +:corpses: All vermin remains and non-entombed corpses. This includes + former members of your fort, so if this category is enabled, + dwarves that have fallen down your well will rot away with + time. +:usable-parts: Non-entombed body parts that can be used for manufacturing, + crafting, or suturing (e.g. hair, wool, skulls, horns, etc.). +:unusable-parts: Non-entombed body parts that can't be used for manufacturing + or crafting. +:parts: Shorthand for the combination of the above two categories. +:all: Shorthand for all of the above categories. + +When setting a frequency, the number indicates the number of days between +adjustments of the deterioration counter. The default frequency of 1 day will +result in items disappearing after several months. The number does not need to +be a whole number. E.g. ``deteriorate frequency 0.5 all`` is perfectly valid. Examples -------- -Start deteriorating corpses and body parts, keeping usable parts such as hair, wool:: - - deteriorate start --types corpses --keep-usable - -Start deteriorating corpses and food and do it at twice the default rate:: - - deteriorate start --types corpses,food --freq 0.5,days - -Deteriorate corpses quickly but clothes slowly:: - - deteriorate start -tcorpses -f0.1 - deteriorate start -tclothes -f3,months - -Options -------- - -``-f``, ``--freq``, ``--frequency [,]`` - How often to increment the wear counters. ```` can be one of - ``days``, ``months``, or ``years`` and defaults to ``days`` if not - specified. The default frequency of 1 day will result in items disappearing - after several months. The number does not need to be a whole number. E.g. - ``--freq=0.5,days`` is perfectly valid. -``-k``, ``--keep-usable`` - Keep usable body parts such as hair, wool, hooves, bones, and skulls. -``-q``, ``--quiet`` - Silence non-error output. -``-t``, ``--types `` - The comma-separated list of item types to affect. This option is required - for ``start``, ``stop``, and ``now`` commands. - -Types ------ - -:clothes: All clothing pieces that have an armor rating of 0 and are lying on - the ground. -:corpses: All resident corpses and body parts. -:food: All food and plants, regardless of whether they are in barrels or - stockpiles. Seeds are left untouched. +``enable deteriorate`` + Start deteriorating items with current settings. +``deteriorate status`` + Show the current settings. +``deteriorate enable corpses,parts`` + Deteriorate corpses and body parts. This includes potentially useful parts + such as hair or wool, so use them quickly or lose them! +``deteriorate frequency 0.5 all`` + Deteriorate items of the enabled categories at twice the default rate. +``deteriorate frequency 14 clothes`` + Deteriorate clothes very slowly. +``deteriorate now corpses,unusable-parts`` + Deteriorate corpses and unusable body parts immediately. This is useful for + cleaning up after a siege or ambush (maybe after you have buried your own + casualties). +``deteriorate now food`` + Deteriorate all food items immediately. Instant famine! diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 37cd56c4e2..76fbee5c10 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -116,6 +116,13 @@ COMMANDS_BY_IDX = { {command='combine', group='gameplay', mode='repeat', desc='Combine partial stacks in stockpiles into full stacks.', params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}}, + {command='deteriorate', group='gameplay', mode='enable'}, + {command='deteriorate enable all', group='gameplay', mode='run', + desc='Enable if you want deteriorate to run on all supported categories instead of just corpses.'}, + {command='deteriorate frequency 0.15 all', group='gameplay', mode='run', + desc='Enable if you want to slow item deterioration down so they take about a year to rot away.'}, + {command='deteriorate frequency 3 all', group='gameplay', mode='run', + desc='Enable if you want to speed item deterioration up so they take less than a month to rot away.'}, {command='dwarfvet', group='gameplay', mode='enable'}, {command='eggs-fertile', help_command='tweak', group='gameplay', mode='tweak', default=true, desc='Displays an indicator on fertile eggs.'}, From 1cd36fbd431a76488f3e5f32781026818dce6ec5 Mon Sep 17 00:00:00 2001 From: Myk Date: Sat, 3 May 2025 10:49:41 -0700 Subject: [PATCH 557/811] Update remove-stress.lua --- remove-stress.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/remove-stress.lua b/remove-stress.lua index 40ce4946a3..251ab774ae 100644 --- a/remove-stress.lua +++ b/remove-stress.lua @@ -1,7 +1,6 @@ -- Sets stress to negative one million --By Putnam; http://www.bay12forums.com/smf/index.php?topic=139553.msg5820486#msg5820486 --edited by Bumber ---edited by BlakeMW --@module = true local utils = require('utils') From 89ce186a9236f78ead947889d74acaba99ad6651 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 3 May 2025 11:08:27 -0700 Subject: [PATCH 558/811] fix doc spacing to make sphinx happy --- docs/gui/design.rst | 55 ++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 28 deletions(-) diff --git a/docs/gui/design.rst b/docs/gui/design.rst index 2106c085e2..e858fc3d09 100644 --- a/docs/gui/design.rst +++ b/docs/gui/design.rst @@ -21,40 +21,39 @@ Shapes ------ - Rectangle - - They can be hollow or filled using 'h'. - - When hollow line thickness can be increased/decreased using 'T'/'t'. - - They can be inverted using 'i'. + - They can be hollow or filled using 'h'. + - When hollow line thickness can be increased/decreased using 'T'/'t'. + - They can be inverted using 'i'. - Ellipse - - They can be hollow or filled using 'h'. - - When hollow line thickness can be increased/decreased using 'T'/'t'. - - They can be inverted using 'i'. + - They can be hollow or filled using 'h'. + - When hollow line thickness can be increased/decreased using 'T'/'t'. + - They can be inverted using 'i'. - Star - - The default has 5 points, use 'B'/'b' to increase/decrease points. - - They can be hollow or filled using 'h'. - - When hollow line thickness can be increased/decreased using 'T'/'t'. - - They can be inverted using 'i'. - - The next-point offset can be increased/decreased using 'N'/'n' which - particularly affects 7 point stars and above to make them spikier or - smoother, but can also be used to decrease to 1 to make symmetrical - polygons or increase to N which only paints the vertexes. - - The orientation can be changed by adding a main axis point using 'v' and - moving this to point in the desired direction. + - The default has 5 points, use 'B'/'b' to increase/decrease points. + - They can be hollow or filled using 'h'. + - When hollow line thickness can be increased/decreased using 'T'/'t'. + - They can be inverted using 'i'. + - The next-point offset can be increased/decreased using 'N'/'n' which + particularly affects 7 point stars and above to make them spikier or + smoother, but can also be used to decrease to 1 to make symmetrical + polygons or increase to N which only paints the vertexes. + - The orientation can be changed by adding a main axis point using 'v' and + moving this to point in the desired direction. - Rows - - Vertical rows can be toggled using 'v'. - - Horizontal rows can be toggled using 'h'. - - Spacing can be increased/decreased using 'T'/'t'. - - They can be inverted using 'i'. + - Vertical rows can be toggled using 'v'. + - Horizontal rows can be toggled using 'h'. + - Spacing can be increased/decreased using 'T'/'t'. + - They can be inverted using 'i'. - Diagonal - - Direction can be reversed using 'r'. - - Spacing can be increased/decreased using 'T'/'t'. - - They can be inverted using 'i'. + - Direction can be reversed using 'r'. + - Spacing can be increased/decreased using 'T'/'t'. + - They can be inverted using 'i'. - Line - - Line thickness can be increased/decreased using 'T'/'t'. - - Can be curved by adding one or more control points using 'v'. + - Line thickness can be increased/decreased using 'T'/'t'. + - Can be curved by adding one or more control points using 'v'. - FreeForm - - Can be toggled open multi-line sequence or closed polygon using 'y' - - Line thickness can be increased/decreased using 'T'/'t'. - + - Can be toggled open multi-line sequence or closed polygon using 'y' + - Line thickness can be increased/decreased using 'T'/'t'. Overlay ------- From 2d5df1ccf6bc55c2af05590747c014adce90b06e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 3 May 2025 15:26:12 -0700 Subject: [PATCH 559/811] add zone to the phase list --- changelog.txt | 1 + gui/blueprint.lua | 20 +++++--------------- 2 files changed, 6 insertions(+), 15 deletions(-) diff --git a/changelog.txt b/changelog.txt index fd45f955d5..0e19e4d32d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,7 @@ Template for new versions: ## New Features - `gui/mod-manager`: when run in a loaded world, shows a list of active mods -- click to export the list to the clipboard for easy sharing or posting +- `gui/blueprint`: now records zone designations ## Fixes - `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session diff --git a/gui/blueprint.lua b/gui/blueprint.lua index 2df37f0106..cde79b10ba 100644 --- a/gui/blueprint.lua +++ b/gui/blueprint.lua @@ -173,22 +173,12 @@ function PhasesPanel:init() subviews={widgets.ToggleHotkeyLabel{view_id='place_phase', frame={t=0, l=0, w=19}, key='CUSTOM_P', label='place', initial_option=self:get_default('place'), label_width=9}, --- widgets.ToggleHotkeyLabel{view_id='zone_phase', --- frame={t=0, l=15, w=19}, --- key='CUSTOM_Z', label='zone', --- initial_option=self:get_default('zone'), --- label_width=5} + widgets.ToggleHotkeyLabel{view_id='zone_phase', + frame={t=0, l=19, w=19}, + key='CUSTOM_Z', label='zone', + initial_option=self:get_default('zone'), + label_width=5} }}, --- widgets.Panel{frame={h=1}, --- subviews={widgets.ToggleHotkeyLabel{view_id='query_phase', --- frame={t=0, l=0, w=19}, --- key='CUSTOM_Q', label='query', --- initial_option=self:get_default('query')}, --- widgets.ToggleHotkeyLabel{view_id='rooms_phase', --- frame={t=0, l=15, w=19}, --- key='CUSTOM_SHIFT_Q', label='rooms', --- initial_option=self:get_default('rooms')} --- }}, widgets.TooltipLabel{ text_to_wrap='Select blueprint phases to export.', show_tooltip=true}, From f1e74867f22c5f51eb4bfe380744b43f062d0b02 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sat, 3 May 2025 17:39:55 -0500 Subject: [PATCH 560/811] add prefer nicknamed (#1430) * add prefer nicknamed * Update changelog.txt * make it look better --------- Co-authored-by: Myk --- changelog.txt | 1 + gui/spectate.lua | 29 +++++++++++++++-------------- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/changelog.txt b/changelog.txt index 0e19e4d32d..3ca33c1fa0 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: - `modtools/moddable-gods`: (reinstated) create new deities from scratch ## New Features +- `gui/spectate`: added "Prefer nicknamed" to the list of options - `gui/mod-manager`: when run in a loaded world, shows a list of active mods -- click to export the list to the clipboard for easy sharing or posting - `gui/blueprint`: now records zone designations diff --git a/gui/spectate.lua b/gui/spectate.lua index 3e7bbd09b8..558e281b5f 100644 --- a/gui/spectate.lua +++ b/gui/spectate.lua @@ -46,7 +46,7 @@ end Spectate = defclass(Spectate, widgets.Window) Spectate.ATTRS { frame_title='Spectate', - frame={l=5, t=5, w=36, h=41}, + frame={l=5, t=5, w=36, h=42}, } local function create_toggle_button(frame, cfg_elem, hotkey, label, cfg_elem_key) @@ -199,36 +199,37 @@ function Spectate:init() create_toggle_button({t=11}, 'include-wildlife', 'CUSTOM_ALT_W', rpad('Include wildlife', lWidth)), create_toggle_button({t=12}, 'prefer-conflict', 'CUSTOM_ALT_B', rpad('Prefer conflict', lWidth)), create_toggle_button({t=13}, 'prefer-new-arrivals', 'CUSTOM_ALT_N', rpad('Prefer new arrivals', lWidth)), + create_toggle_button({t=14}, 'prefer-nicknamed', 'CUSTOM_ALT_I', rpad('Prefer nicknamed', lWidth)), widgets.Divider{ - frame={t=15, h=1}, + frame={t=16, h=1}, frame_style=gui.FRAME_THIN, frame_style_l=false, frame_style_r=false, }, widgets.Label{ - frame={t=17, l=0}, + frame={t=18, l=0}, text="Tooltips:" }, ToggleLabel{ - frame={t=17, l=12}, + frame={t=18, l=12}, initial_option=overlay.isOverlayEnabled(OVERLAY_NAME), on_change=function(val) dfhack.run_command('overlay', val and 'enable' or 'disable', OVERLAY_NAME) end, key='CUSTOM_ALT_O', label="Overlay ", }, widgets.Label{ - frame={t=19, l=colFollow}, + frame={t=20, l=colFollow}, text='Follow', }, widgets.Label{ - frame={t=19, l=colHover}, + frame={t=20, l=colHover}, text='Hover', }, - create_row({t=21}, 'Enabled', 'E', '', colFollow, colHover), + create_row({t=22}, 'Enabled', 'E', '', colFollow, colHover), - create_numeric_edit_field({t=23}, 'tooltip-follow-blink-milliseconds', 'CUSTOM_B', 'Blink period (ms): '), + create_numeric_edit_field({t=24}, 'tooltip-follow-blink-milliseconds', 'CUSTOM_B', 'Blink period (ms): '), widgets.CycleHotkeyLabel{ - frame={t=24}, + frame={t=25}, key='CUSTOM_C', label="Hold to show:", options={ @@ -241,11 +242,11 @@ function Spectate:init() on_change=function(new, _) dfhack.run_command('spectate', 'set', 'tooltip-follow-hold-to-show', new) end }, - create_row({t=26}, 'Job', 'J', 'job', colFollow, colHover), - create_row({t=27}, 'Activity', 'A', 'activity', colFollow, colHover), - create_row({t=28}, 'Name', 'N', 'name', colFollow, colHover), - create_row({t=29}, 'Stress', 'S', 'stress', colFollow, colHover), - create_stress_list({t=30}, colFollow, colHover), + create_row({t=27}, 'Job', 'J', 'job', colFollow, colHover), + create_row({t=28}, 'Activity', 'A', 'activity', colFollow, colHover), + create_row({t=29}, 'Name', 'N', 'name', colFollow, colHover), + create_row({t=30}, 'Stress', 'S', 'stress', colFollow, colHover), + create_stress_list({t=31}, colFollow, colHover), } end From d73e2dc1afd205272d5ade3a47b72d9bf5998e6d Mon Sep 17 00:00:00 2001 From: Chris Johnsen Date: Sat, 3 May 2025 21:23:04 -0500 Subject: [PATCH 561/811] confirm: allow multiple confirmations to be paused Allowing only a single, mutually exclusive, paused confirmation was likely to be confusing. Per review from lethosor. --- changelog.txt | 2 +- confirm.lua | 16 ++++++++++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/changelog.txt b/changelog.txt index 6b3d279ae1..6d14f32e86 100644 --- a/changelog.txt +++ b/changelog.txt @@ -42,7 +42,7 @@ Template for new versions: - `confirm`: when removing a manager order, show correct order description when using non-100% interface setting - `confirm`: when removing a manager order, show correct order description after prior order removal or window resize (when scrolled to bottom of order list) - `confirm`: when removing a manager order, show specific item/job type for ammo, shield, helm, gloves, shoes, trap component, and meal orders -- `confirm`: the pause option now only pauses future instances of the current confirmation instead of all confirmations in the current context +- `confirm`: the pause option now pauses individual confirmation types, allowing multiple different confirmations to be paused independently ## Misc Improvements diff --git a/confirm.lua b/confirm.lua index 5d1944d29b..5142013d1f 100644 --- a/confirm.lua +++ b/confirm.lua @@ -63,6 +63,7 @@ function ConfirmOverlay:init() } end end + self.paused_confs = {} end function ConfirmOverlay:preUpdateLayout() @@ -77,11 +78,14 @@ function ConfirmOverlay:preUpdateLayout() end function ConfirmOverlay:overlay_onupdate() - if self.paused_conf and - not dfhack.gui.matchFocusString(self.paused_conf.context, + for conf in pairs(self.paused_confs) do + if not dfhack.gui.matchFocusString(conf.context, dfhack.gui.getDFViewscreen(true)) - then - self.paused_conf = nil + then + self.paused_confs[conf] = nil + end + end + if not next(self.paused_confs) then self.overlay_onupdate_max_freq_seconds = 300 end end @@ -114,7 +118,7 @@ function ConfirmOverlay:onInput(keys) local scr = dfhack.gui.getDFViewscreen(true) for id, conf in pairs(specs.REGISTRY) do if specs.config.data[id].enabled and self:matches_conf(conf, keys, scr) then - if conf == self.paused_conf then + if self.paused_confs[conf] then return false end local mouse_pos = xy2pos(dfhack.screen.getMousePos()) @@ -123,7 +127,7 @@ function ConfirmOverlay:onInput(keys) conf.on_propagate() end if pause then - self.paused_conf = conf + self.paused_confs[conf] = true self.overlay_onupdate_max_freq_seconds = 0 end if keys._MOUSE_L then From 989393cc27b69f0d20eacffb9d0a87ac14aa5ade Mon Sep 17 00:00:00 2001 From: 83N170 <83N170@mail.com> Date: Sun, 4 May 2025 20:35:18 +0100 Subject: [PATCH 562/811] Fix line thickness extending outside map --- internal/design/shapes.lua | 84 +++++++++++++++----------------------- 1 file changed, 33 insertions(+), 51 deletions(-) diff --git a/internal/design/shapes.lua b/internal/design/shapes.lua index c6d3c9debd..81ee41883f 100644 --- a/internal/design/shapes.lua +++ b/internal/design/shapes.lua @@ -404,6 +404,19 @@ end LineDrawer = defclass(LineDrawer, Shape) +function LineDrawer:plot_thickness(x, y, thickness) + local map_width, map_height = dfhack.maps.getTileSize() + for i = math.max(y - math.floor(thickness / 2), 0), math.min(y + math.ceil(thickness / 2) - 1, map_height - 1) do + for j = math.max(x - math.floor(thickness / 2), 0), math.min(x + math.ceil(thickness / 2) - 1, map_width - 1) do + if not self.arr[j] then self.arr[j] = {} end + if not self.arr[j][i] then + self.arr[j][i] = true + self.num_tiles = self.num_tiles + 1 + end + end + end +end + function LineDrawer:plot_bresenham(x0, y0, x1, y1, thickness) local dx = math.abs(x1 - x0) local dy = math.abs(y1 - y0) @@ -411,34 +424,26 @@ function LineDrawer:plot_bresenham(x0, y0, x1, y1, thickness) local sy = y0 < y1 and 1 or -1 local e2, x, y - for i = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do - x = x0 - y = y0 + i - local err = dx - dy - while true do - for j = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do - if not self.arr[x + j] then self.arr[x + j] = {} end - if not self.arr[x + j][y] then - self.arr[x + j][y] = true - self.num_tiles = self.num_tiles + 1 - end - end + x = x0 + y = y0 + local err = dx - dy + while true do + self:plot_thickness(x, y, thickness) - if sx * x >= sx * x1 and sy * y >= sy * (y1 + i) then - break - end + if sx * x >= sx * x1 and sy * y >= sy * y1 then + break + end - e2 = 2 * err + e2 = 2 * err - if e2 > -dy then - err = err - dy - x = x + sx - end + if e2 > -dy then + err = err - dy + x = x + sx + end - if e2 < dx then - err = err + dx - y = y + sy - end + if e2 < dx then + err = err + dx + y = y + sy end end end @@ -490,15 +495,7 @@ function Line:cubic_bezier(x0, y0, x1, y1, bezier_point1, bezier_point2, thickne 0.5) local y = math.floor(((1 - t) ^ 3 * y0 + 3 * (1 - t) ^ 2 * t * y2 + 3 * (1 - t) * t ^ 2 * y3 + t ^ 3 * y1) + 0.5) - for i = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do - for j = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do - if not self.arr[x + j] then self.arr[x + j] = {} end - if not self.arr[x + j][y + i] then - self.arr[x + j][y + i] = true - self.num_tiles = self.num_tiles + 1 - end - end - end + self:plot_thickness(x, y, thickness) t = t + granularity end @@ -507,15 +504,8 @@ function Line:cubic_bezier(x0, y0, x1, y1, bezier_point1, bezier_point2, thickne 0.5) local y_end = math.floor(((1 - 1) ^ 3 * y0 + 3 * (1 - 1) ^ 2 * 1 * y2 + 3 * (1 - 1) * 1 ^ 2 * y3 + 1 ^ 3 * y1) + 0.5) - for i = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do - for j = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do - if not self.arr[x_end + j] then self.arr[x_end + j] = {} end - if not self.arr[x_end + j][y_end + i] then - self.arr[x_end + j][y_end + i] = true - self.num_tiles = self.num_tiles + 1 - end - end - end + + self:plot_thickness(x_end, y_end, thickness) end function Line:quadratic_bezier(x0, y0, x1, y1, bezier_point1, thickness) @@ -525,15 +515,7 @@ function Line:quadratic_bezier(x0, y0, x1, y1, bezier_point1, thickness) while t <= 1 do local x = math.floor(((1 - t) ^ 2 * x0 + 2 * (1 - t) * t * x2 + t ^ 2 * x1) + 0.5) local y = math.floor(((1 - t) ^ 2 * y0 + 2 * (1 - t) * t * y2 + t ^ 2 * y1) + 0.5) - for i = 0, thickness - 1 do - for j = -math.floor(thickness / 2), math.ceil(thickness / 2) - 1 do - if not self.arr[x + j] then self.arr[x + j] = {} end - if not self.arr[x + j][y + i] then - self.arr[x + j][y + i] = true - self.num_tiles = self.num_tiles + 1 - end - end - end + self:plot_thickness(x, y, thickness) t = t + granularity end end From 65e130c2d8bc454b6772796c2257a1b51f38806b Mon Sep 17 00:00:00 2001 From: 83N170 <83N170@mail.com> Date: Sun, 4 May 2025 20:37:03 +0100 Subject: [PATCH 563/811] Update changelog --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index 3ca33c1fa0..0678eabe68 100644 --- a/changelog.txt +++ b/changelog.txt @@ -20,6 +20,8 @@ Template for new versions: ## Fixes +- `gui/design`: prevent line thickness from extending outside the map boundary + ## Misc Improvements ## Removed From 9f76bc9d2a1c1e1cb237543f320acda352f40881 Mon Sep 17 00:00:00 2001 From: Myk Date: Sun, 4 May 2025 12:59:03 -0700 Subject: [PATCH 564/811] Fix formatting in justice.rst --- docs/justice.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/justice.rst b/docs/justice.rst index ea65a668d2..99ca893873 100644 --- a/docs/justice.rst +++ b/docs/justice.rst @@ -12,6 +12,7 @@ Usage ----- :: + justice [list] justice pardon [--unit ] From 1db1efcce532e8fdb9c613a90abe282763feeb9c Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 19:15:56 +0000 Subject: [PATCH 565/811] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.32.1 → 0.33.0](https://github.com/python-jsonschema/check-jsonschema/compare/0.32.1...0.33.0) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f601d4c321..afa4a6dee2 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.32.1 + rev: 0.33.0 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From d9722e519950a18e88c5857c94901fa901ef012f Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Mon, 5 May 2025 18:55:41 -0700 Subject: [PATCH 566/811] fix usable parts check and also fix game load logic --- deteriorate.lua | 35 +++++++++++++++++++++++++++-------- 1 file changed, 27 insertions(+), 8 deletions(-) diff --git a/deteriorate.lua b/deteriorate.lua index 2d6aa0164a..42c0498aad 100644 --- a/deteriorate.lua +++ b/deteriorate.lua @@ -113,13 +113,30 @@ local function is_valid_corpse(item) return not is_entombed(item) end +local usable_types = { + 'plant', + 'silk', + 'leather', + 'bone', + 'shell', + 'wood', + 'soap', + 'tooth', + 'horn', + 'pearl', + 'skull', + 'hair_wool', + 'yarn', +} + local function is_usable_corpse_piece(item) - return item.corpse_flags.hair_wool or - item.corpse_flags.pearl or - item.corpse_flags.plant or - item.corpse_flags.shell or - item.corpse_flags.silk or - item.corpse_flags.yarn + if item.flags.dead_dwarf or item.corpse_flags.unbutchered then + return false + end + for _,flag in ipairs(usable_types) do + if item.corpse_flags[flag] then return true end + end + return false end local function is_valid_usable_corpse_piece(item) @@ -256,7 +273,9 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) state = get_default_state() utils.assign(state, dfhack.persistent.getSiteData(GLOBAL_KEY, state)) - event_loop() + for _,category in ipairs(categories) do + event_loop(category) + end end --------------------- @@ -297,7 +316,7 @@ local function status() local running_str = state.enabled and 'Running' or 'Would run' print(('deteriorate is %s'):format(state.enabled and 'enabled' or 'disabled')) print() - for _,category in pairs(categories) do + for _,category in ipairs(categories) do local status_str = 'Stopped' local category_data = state.categories[category] if category_data.enabled then From a193969f27050581daecd49df63f2df8f2843f1f Mon Sep 17 00:00:00 2001 From: Ryan Williams Date: Wed, 7 May 2025 22:22:23 -0700 Subject: [PATCH 567/811] Update adv-finder.lua - "Unnamed" fits DF better --- gui/adv-finder.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/adv-finder.lua b/gui/adv-finder.lua index cfd17b7646..ccb3cd664c 100644 --- a/gui/adv-finder.lua +++ b/gui/adv-finder.lua @@ -31,7 +31,7 @@ end function get_hf_name(hf) --'Native Name "Translated Name", Race' local full_name = transName(hf.name, false) if full_name == '' then --Improve searchability - full_name = 'Anonymous' + full_name = 'Unnamed' else --Add the translation local t_name = transName(hf.name, true) if full_name ~= t_name then --Don't repeat @@ -50,7 +50,7 @@ end function get_art_name(ar) --'Native Name "Translated Name", Item' local full_name = transName(ar.name, false) if full_name == '' then --Improve searchability - full_name = 'Anonymous' + full_name = 'Unnamed' else --Add the translation local t_name = transName(ar.name, true) if full_name ~= t_name then --Don't repeat @@ -539,7 +539,7 @@ end local function insert_name_text(t, name) --HF or artifact name; Return true if both lines local str = transName(name, false) if str == '' then - table.insert(t, 'Anonymous') + table.insert(t, 'Unnamed') else --Both native and translation table.insert(t, str) --Native local t_name = transName(name, true) From 8a2c8e7b13d703da672532d408e4250dd11a86c9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Thu, 26 Jun 2025 05:30:38 -0700 Subject: [PATCH 568/811] update changelog for 51.12 --- changelog.txt | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/changelog.txt b/changelog.txt index 0678eabe68..9f0324d3aa 100644 --- a/changelog.txt +++ b/changelog.txt @@ -16,12 +16,8 @@ Template for new versions: ## New Features -- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction - ## Fixes -- `gui/design`: prevent line thickness from extending outside the map boundary - ## Misc Improvements ## Removed @@ -30,6 +26,18 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 51.12-r1 + ## New Tools - `deteriorate`: (reinstated) allow corpses, body parts, food, and/or damaged clothes to rot away - `modtools/moddable-gods`: (reinstated) create new deities from scratch @@ -38,16 +46,16 @@ Template for new versions: - `gui/spectate`: added "Prefer nicknamed" to the list of options - `gui/mod-manager`: when run in a loaded world, shows a list of active mods -- click to export the list to the clipboard for easy sharing or posting - `gui/blueprint`: now records zone designations +- `gui/design`: add option to draw N-point stars, hollow or filled or inverted, and change the main axis to orient in any direction ## Fixes - `starvingdead`: properly restore to correct enabled state when loading a new game that is different from the first game loaded in this session - `starvingdead`: ensure undead decay does not happen faster than the declared decay rate when saving and loading the game +- `gui/design`: prevent line thickness from extending outside the map boundary ## Misc Improvements - `remove-stress`: also applied to long-term stress, immediately removing stressed and haggard statuses -## Removed - # 51.11-r1 ## Fixes From a719636f8273f5d646058edac6ee7976f1359a70 Mon Sep 17 00:00:00 2001 From: Louis Hong Date: Thu, 26 Jun 2025 14:07:23 -0700 Subject: [PATCH 569/811] bug fix: journal.lua minor performance lost due to typo "colllapsed" obvious typo causing unintended performance lost --- gui/journal.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/journal.lua b/gui/journal.lua index 4faacb2183..03cf34b022 100644 --- a/gui/journal.lua +++ b/gui/journal.lua @@ -72,7 +72,7 @@ function JournalWindow:init() self.subviews.table_of_contents_panel.visible = not collapsed self.subviews.table_of_contents_divider.visible = not collapsed - if not colllapsed then + if not collapsed then self:reloadTableOfContents() end From 2511a57f2702e99ea945e2005c4d6e3ad8c0aade Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 27 Jun 2025 12:49:31 -0500 Subject: [PATCH 570/811] remove `think_counter` from `gui/gm-unit` for 51.12 compatibility --- internal/gm-unit/editor_counters.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/gm-unit/editor_counters.lua b/internal/gm-unit/editor_counters.lua index 0bf258df1e..c740bcfb32 100644 --- a/internal/gm-unit/editor_counters.lua +++ b/internal/gm-unit/editor_counters.lua @@ -9,7 +9,6 @@ Editor_Counters=defclass(Editor_Counters, base_editor.Editor) Editor_Counters.ATTRS{ frame_title = "Counters editor", counters1={ - "think_counter", "job_counter", "swap_counter", "winded", From e8267fcbd4892d8120332ca466b77dd71c0970ac Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sat, 28 Jun 2025 17:27:21 -0500 Subject: [PATCH 571/811] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 9f0324d3aa..d669e7230b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: ## New Tools ## New Features +- `deathcause`: added functionality to this script to fetch cause of death programatically ## Fixes From 7ec2c9ec8581259266c16f2a7de7b0fbc58ed565 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 29 Jun 2025 09:15:59 -0500 Subject: [PATCH 572/811] mod-manager.lua: do not except on missing mod metadata fixes dfhack/dfhack#5489 --- gui/mod-manager.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 792715c6d1..7e6b6efe72 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -524,7 +524,8 @@ function ModlistWindow:refresh_list() local choices = {} for idx,mod in ipairs(scriptmanager.get_active_mods()) do if not include_vanilla and mod.vanilla then goto continue end - local steam_id = scriptmanager.get_mod_info_metadata(mod.path, 'STEAM_FILE_ID').STEAM_FILE_ID + local metadata = scriptmanager.get_mod_info_metadata(mod.path, 'STEAM_FILE_ID') + local steam_id = metadata and metadata.STEAM_FILE_ID or nil local url = steam_id and (': https://steamcommunity.com/sharedfiles/filedetails/?id=%s'):format(steam_id) or '' table.insert(choices, { text={ From 71194edf327dc9f725a931b19227249e04b0e03e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 29 Jun 2025 09:27:50 -0500 Subject: [PATCH 573/811] add changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 9f0324d3aa..c6dbbe078c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files ## Misc Improvements From 4992aa329942d91507209e65368aae3dd35cfc49 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 29 Jun 2025 10:12:31 -0500 Subject: [PATCH 574/811] add changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 9f0324d3aa..f6a2fda3f7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 ## Misc Improvements From 9f9ae7085b4d5d545e63d49ab26f683d97e9aa86 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Mon, 30 Jun 2025 12:19:30 -0500 Subject: [PATCH 575/811] add api docs --- docs/deathcause.rst | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/docs/deathcause.rst b/docs/deathcause.rst index c9a2ae0a06..20dddb11e6 100644 --- a/docs/deathcause.rst +++ b/docs/deathcause.rst @@ -14,3 +14,29 @@ Usage :: deathcause + +API +--- + +The ``deathcause`` script can be called programmatically by other scripts, either via the +commandline interface with ``dfhack.run_script()`` or via the API functions +defined in :source-scripts:`deathcause.lua`, available from the return value of +``reqscript('deathcause')``: + +* ``getDeathCauseFromHistFig(histfig)`` + +Returns a string with the historical figure's cause of death, sometimes with more information +than with a unit. + +* ``getDeathCauseFromUnit(unit)`` + +Returns a string with the unit's cause of death. + + API usage example:: + + local dc = reqscript('deathcause') + + -- Note: this is an arguably bad example because this is the same as running deathcause + -- from the launcher, but this would theoretically still work. + local deathReason = dc.getDeathCauseFromUnit(dfhack.gui.getSelectedUnit()) + print(deathReason) From 3c599e263ba9340bb48b3311b604df60cc24c350 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Wed, 2 Jul 2025 16:24:59 +0200 Subject: [PATCH 576/811] resolve overlap with new buttons in 51.13 --- changelog.txt | 1 + uniform-unstick.lua | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index abb2dbbc4f..561f4e3c48 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files +- `uniform-unstick`: resolve overlap with new buttons in 51.13 ## Misc Improvements diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 73fd78b9aa..0fb501fd2d 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -343,10 +343,11 @@ local MIN_WIDTH = 26 EquipOverlay = defclass(EquipOverlay, overlay.OverlayWidget) EquipOverlay.ATTRS{ desc='Adds a link to the equip screen to fix equipment conflicts.', - default_pos={x=7,y=21}, + default_pos={x=7,y=23}, default_enabled=true, viewscreens='dwarfmode/Squads/Equipment/Default', frame={w=MIN_WIDTH, h=1}, + version=1 } function EquipOverlay:init() From 28cef1686412a0d2c933485b694c15ba53963988 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 2 Jul 2025 13:47:10 -0500 Subject: [PATCH 577/811] add changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 9f0324d3aa..9f267bc5d9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed ## Misc Improvements From 800ae8321b81558ff9522a51c444287126389bb6 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 2 Jul 2025 13:48:54 -0500 Subject: [PATCH 578/811] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index c794f6d312..ba51e4f81d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -32,7 +32,7 @@ Template for new versions: ## New Features ## Fixes -- `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed +- `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files - `uniform-unstick`: resolve overlap with new buttons in 51.13 From 1b95334b2ee7d40a686be17bd343ecbf55504e4c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 3 Jul 2025 16:56:16 -0500 Subject: [PATCH 579/811] remove fake `curse` compound in `unitst` resolves an alignment issue in `unitst` --- changelog.txt | 1 + devel/export-dt-ini.lua | 6 +++--- devel/make-dt.pl | 6 +++--- dwarf-op.lua | 2 +- fix/noexert-exhaustion.lua | 4 ++-- immortal-cravings.lua | 4 ++-- starvingdead.lua | 2 +- 7 files changed, 13 insertions(+), 12 deletions(-) diff --git a/changelog.txt b/changelog.txt index ba51e4f81d..a937e2fb20 100644 --- a/changelog.txt +++ b/changelog.txt @@ -18,6 +18,7 @@ Template for new versions: ## Fixes - `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 +- fixed references to removed ``unit.curse`` compound ## Misc Improvements diff --git a/devel/export-dt-ini.lua b/devel/export-dt-ini.lua index ad4d5bfbde..84e1da5cf6 100644 --- a/devel/export-dt-ini.lua +++ b/devel/export-dt-ini.lua @@ -318,9 +318,9 @@ address('physical_attrs',df.unit,'body','physical_attrs') address('body_size',df.unit,'appearance','body_modifiers') address('size_info',df.unit,'body','size_info','size_cur') address('size_base',df.unit,'body','size_info','size_base') -address('curse',df.unit,'curse','name') -address('curse_add_flags1',df.unit,'curse','add_tags1') -address('turn_count',df.unit,'curse','interaction','time_on_site') +address('curse',df.unit,'uwss_display_name_string') +address('curse_add_flags1',df.unit,'uwss_add_caste_flag') +address('turn_count',df.unit,'usable_interaction','time_on_site') address('souls',df.unit,'status','souls') address('states',df.unit,'status','misc_traits') address('labors',df.unit,'status','labors') diff --git a/devel/make-dt.pl b/devel/make-dt.pl index 4bffeb0d55..9d8ba2c433 100755 --- a/devel/make-dt.pl +++ b/devel/make-dt.pl @@ -325,9 +325,9 @@ ($$$$) emit_addr 'physical_attrs',%all,'unit','body.physical_attrs'; emit_addr 'body_size',%all,'unit','appearance.body_modifiers'; emit_addr 'size_info',%all,'unit','body.size_info'; - emit_addr 'curse',%all,'unit','curse.name'; - emit_addr 'curse_add_flags1',%all,'unit','curse.add_tags1'; - emit_addr 'turn_count',%all,'unit','curse.time_on_site'; + emit_addr 'curse',%all,'unit','uwss_display_name_sing'; + emit_addr 'curse_add_flags1',%all,'unit','uwss_add_caste_flag'; + emit_addr 'turn_count',%all,'unit','usable_interaction.time_on_site'; emit_addr 'souls',%all,'unit','status.souls'; emit_addr 'states',%all,'unit','status.misc_traits'; emit_addr 'labors',%all,'unit','status.labors'; diff --git a/dwarf-op.lua b/dwarf-op.lua index 3581e3004c..45b7233263 100644 --- a/dwarf-op.lua +++ b/dwarf-op.lua @@ -733,7 +733,7 @@ local seasons = { 'winter', } function GetWave(dwf) - arrival_time = current_tick - dwf.curse.interaction.time_on_site; + arrival_time = current_tick - dwf.useable_interaction.time_on_site; --print(string.format("Current year %s, arrival_time = %s, ticks_per_year = %s", df.global.cur_year, arrival_time, ticks_per_year)) arrival_year = df.global.cur_year + (arrival_time // ticks_per_year); arrival_season = 1 + (arrival_time % ticks_per_year) // ticks_per_season; diff --git a/fix/noexert-exhaustion.lua b/fix/noexert-exhaustion.lua index 44ea9d506f..eabfd965ef 100644 --- a/fix/noexert-exhaustion.lua +++ b/fix/noexert-exhaustion.lua @@ -5,10 +5,10 @@ --Running this script on repeat approximately at least every 350 ticks should prevent NOEXERT units from becoming Tired as a result of Individual Combat Drill. function isNoExert(u) - if(u.curse.rem_tags1.NOEXERT) then --tag removal overrides tag addition, so if the NOEXERT tag is removed the unit cannot be NOEXERT. + if(u.uwss_remove_caste_flag.NOEXERT) then --tag removal overrides tag addition, so if the NOEXERT tag is removed the unit cannot be NOEXERT. return false end - if(u.curse.add_tags1.NOEXERT) then--if the tag hasn't been removed, and the unit has a curse that adds it, they must be NOEXERT. + if(u.uwss_add_caste_flag.NOEXERT) then--if the tag hasn't been removed, and the unit has a curse that adds it, they must be NOEXERT. return true end if(dfhack.units.casteFlagSet(u.race,u.caste, df.caste_raw_flags.NOEXERT)) then --if the tag hasn't been added or removed, but their race and caste has the tag, they're NOEXERT. diff --git a/immortal-cravings.lua b/immortal-cravings.lua index 2b76ee4646..2162045fd2 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -172,8 +172,8 @@ local function unit_loop() end local function is_active_caste_flag(unit, flag_name) - return not unit.curse.rem_tags1[flag_name] and - (unit.curse.add_tags1[flag_name] or dfhack.units.casteFlagSet(unit.race, unit.caste, df.caste_raw_flags[flag_name])) + return not unit.uwss_remove_caste_flag[flag_name] and + (unit.uwss_add_caste_flag[flag_name] or dfhack.units.casteFlagSet(unit.race, unit.caste, df.caste_raw_flags[flag_name])) end ---main loop: look for citizens with personality needs for food/drink but w/o physiological need diff --git a/starvingdead.lua b/starvingdead.lua index 5518676ad1..b61db20de0 100644 --- a/starvingdead.lua +++ b/starvingdead.lua @@ -43,7 +43,7 @@ local function do_decay() attribute.value = math.floor(attribute.value - (attribute.value * attribute_decay)) end - if unit.curse.interaction.time_on_site > (state.death_threshold * TICKS_PER_MONTH) then + if unit.usable_interaction.time_on_site > (state.death_threshold * TICKS_PER_MONTH) then unit.animal.vanish_countdown = 1 end end From a8b2f21cd78a417cc2227a69d7eb529bdc2a3302 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Fri, 4 Jul 2025 13:08:18 +0200 Subject: [PATCH 580/811] prioritize high-value meals and don't go eating or drinking on a full stomach --- changelog.txt | 1 + immortal-cravings.lua | 49 ++++++++++++++++++++++++++++++++++++------- 2 files changed, 42 insertions(+), 8 deletions(-) diff --git a/changelog.txt b/changelog.txt index a937e2fb20..142f3f5452 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,6 +36,7 @@ Template for new versions: - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files - `uniform-unstick`: resolve overlap with new buttons in 51.13 +- `immortal-cravings`: prioritize high-value meals and don't go eating or drinking on a full stomach ## Misc Improvements diff --git a/immortal-cravings.lua b/immortal-cravings.lua index 2162045fd2..de251f3fc8 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -13,6 +13,27 @@ function distance(p1, p2) return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) + math.abs(p1.z - p2.z) end +---find best item in an item vector (according to some metric) +---@generic T : df.item +---@param item_vector T[] +---@param metric fun(item: T): number +---@param is_good? fun(item: T): boolean +---@return T? +function findBest(item_vector, metric, is_good) + local best = nil + local mbest = -1 + for _,item in ipairs(item_vector) do + if not item.flags.in_job and (not is_good or is_good(item)) then + mitem = metric(item) + if not best or mitem > mbest then + best = item + mbest = mitem + end + end + end + return best +end + ---find closest accessible item in an item vector ---@generic T : df.item ---@param pos df.coord @@ -46,19 +67,28 @@ local function get_closest_drink(pos) return findClosest(pos, df.global.world.items.other.DRINK, is_good) end ----find some prepared meal +---find highest-value accessible meal ---@return df.item_foodst? -local function get_closest_meal(pos) +local function get_best_meal(pos) + ---@param meal df.item_foodst local function is_good(meal) - if meal.flags.rotten then + local accessible = dfhack.maps.canWalkBetween(pos,xyz2pos(dfhack.items.getPosition(meal))) + if meal.flags.rotten or not accessible then return false else + -- check that meal is either on the ground or in food storage (and not in a backpack) local container = dfhack.items.getContainer(meal) return not container or container:isFoodStorage() end end - return findClosest(pos, df.global.world.items.other.FOOD, is_good) + + ---@param meal df.item_foodst + local function portion_value(meal) + return dfhack.items.getValue(meal) / meal.stack_size + end + + return findBest(df.global.world.items.other.FOOD, portion_value, is_good) end ---create a Drink job for the given unit @@ -86,7 +116,7 @@ end ---create Eat job for the given unit ---@param unit df.unit local function goEat(unit) - local meal = get_closest_meal(unit.pos) + local meal = get_best_meal(unit.pos) if not meal then -- print('no accessible meals found') return @@ -181,12 +211,15 @@ local function main_loop() -- print('immortal-cravings watching:') watched = {} for _, unit in ipairs(dfhack.units.getCitizens()) do - if not is_active_caste_flag(unit, 'NO_DRINK') and not is_active_caste_flag(unit, 'NO_EAT') then + if + not (is_active_caste_flag(unit, 'NO_DRINK') or is_active_caste_flag(unit, 'NO_EAT')) or + unit.counters2.stomach_content > 0 + then goto next_unit end for _, need in ipairs(unit.status.current_soul.personality.needs) do - if need.id == DrinkAlcohol and need.focus_level < threshold or - need.id == EatGoodMeal and need.focus_level < threshold + if need.id == DrinkAlcohol and need.focus_level < threshold or + need.id == EatGoodMeal and need.focus_level < threshold then table.insert(watched, unit.id) -- print(' '..dfhack.df2console(dfhack.units.getReadableName(unit))) From be6f620abf453a55f5ded1a5756c69f736b7390b Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 7 Jul 2025 19:40:42 +0000 Subject: [PATCH 581/811] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/python-jsonschema/check-jsonschema: 0.33.0 → 0.33.2](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.0...0.33.2) --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index afa4a6dee2..2ec6f9ff9f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.0 + rev: 0.33.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks From df9d85a0d1c703d9ec2cbed9d05baebaffc9c2a9 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 03:25:28 +0800 Subject: [PATCH 582/811] Add flexibility for changing vanilla module versions --- gui/mod-manager.lua | 42 +++++++++++++++++++++++++++++++++++++++--- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 7e6b6efe72..183962cdf1 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -12,11 +12,45 @@ local widgets = require('gui.widgets') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' +local vanilla_modules = { + ['vanilla_text'] = true, + ['vanilla_languages'] = true, + ['vanilla_descriptors'] = true, + ['vanilla_materials'] = true, + ['vanilla_environment'] = true, + ['vanilla_plants'] = true, + ['vanilla_items'] = true, + ['vanilla_buildings'] = true, + ['vanilla_bodies'] = true, + ['vanilla_creatures'] = true, + ['vanilla_entities'] = true, + ['vanilla_reactions'] = true, + ['vanilla_interactions'] = true, + ['vanilla_descriptors_graphics'] = true, + ['vanilla_plants_graphics'] = true, + ['vanilla_items_graphics'] = true, + ['vanilla_buildings_graphics'] = true, + ['vanilla_creatures_graphics'] = true, + ['vanilla_interactions_graphics'] = true, + ['vanilla_world_map'] = true, + ['vanilla_interface'] = true, + ['vanilla_music'] = true, +} + +function get_moddable_viewscreen(type) + local vs = nil + if type == 'region' then + vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0) + elseif type == 'arena' then + vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_arenast, 0) + end + return vs +end + -- get_newregion_viewscreen and get_modlist_fields are declared as global functions -- so external tools can call them to get the DF mod list function get_newregion_viewscreen() - local vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0) - return vs + return get_moddable_viewscreen('region') end function get_modlist_fields(kind, viewscreen) @@ -62,7 +96,9 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local mod_index = nil for i, v in ipairs(from_fields.id) do local version = from_fields.numeric_version[i] - if v.value == mod_id and version == mod_version then + local vanilla = vanilla_modules[mod_id] + -- assuming that vanilla mods will not have multiple possible indices + if v.value == mod_id and (vanilla or version == mod_version) then mod_index = i break end From 3b49b24c6ddedf305d18f5575574504fdee263f6 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 04:30:15 +0800 Subject: [PATCH 583/811] Add comments --- gui/mod-manager.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 183962cdf1..784055c4a3 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -12,6 +12,9 @@ local widgets = require('gui.widgets') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' +-- hardly an elegant solution, but mysteriously, +-- using from_fields.src_dir[i].startswith('data/vanilla') in move_mod_entry() +-- leads to lua complaining that it 'cannot read field string.startswith: not found' local vanilla_modules = { ['vanilla_text'] = true, ['vanilla_languages'] = true, @@ -97,7 +100,7 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) for i, v in ipairs(from_fields.id) do local version = from_fields.numeric_version[i] local vanilla = vanilla_modules[mod_id] - -- assuming that vanilla mods will not have multiple possible indices + -- assumes that vanilla mods will not have multiple possible indices. if v.value == mod_id and (vanilla or version == mod_version) then mod_index = i break From cadf06b017a91d316343bac0a5ef3f50ef26116a Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 16:29:31 +0800 Subject: [PATCH 584/811] Edit vanilla mod identification logic --- gui/mod-manager.lua | 39 ++++++++++----------------------------- 1 file changed, 10 insertions(+), 29 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 784055c4a3..832a2c1482 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -12,34 +12,15 @@ local widgets = require('gui.widgets') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' --- hardly an elegant solution, but mysteriously, --- using from_fields.src_dir[i].startswith('data/vanilla') in move_mod_entry() --- leads to lua complaining that it 'cannot read field string.startswith: not found' -local vanilla_modules = { - ['vanilla_text'] = true, - ['vanilla_languages'] = true, - ['vanilla_descriptors'] = true, - ['vanilla_materials'] = true, - ['vanilla_environment'] = true, - ['vanilla_plants'] = true, - ['vanilla_items'] = true, - ['vanilla_buildings'] = true, - ['vanilla_bodies'] = true, - ['vanilla_creatures'] = true, - ['vanilla_entities'] = true, - ['vanilla_reactions'] = true, - ['vanilla_interactions'] = true, - ['vanilla_descriptors_graphics'] = true, - ['vanilla_plants_graphics'] = true, - ['vanilla_items_graphics'] = true, - ['vanilla_buildings_graphics'] = true, - ['vanilla_creatures_graphics'] = true, - ['vanilla_interactions_graphics'] = true, - ['vanilla_world_map'] = true, - ['vanilla_interface'] = true, - ['vanilla_music'] = true, -} +-- Shamelessly taken from hack/library/lua/script-manager.lua +function vanilla(dir) + dir = dir.value + dir = dir -- better safe than sorry i guess + return dir:startswith('data/vanilla') +end +-- get_moddable_viewscreen(), get_any_moddable_viewscreen() and get_modlist_fields are declared +-- as global functions so external tools can call them to get the DF mod list function get_moddable_viewscreen(type) local vs = nil if type == 'region' then @@ -99,9 +80,9 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local mod_index = nil for i, v in ipairs(from_fields.id) do local version = from_fields.numeric_version[i] - local vanilla = vanilla_modules[mod_id] + local src_dir = from_fields.src_dir[i] -- assumes that vanilla mods will not have multiple possible indices. - if v.value == mod_id and (vanilla or version == mod_version) then + if v.value == mod_id and (vanilla(src_dir) or version == mod_version) then mod_index = i break end From f194ffd6c82f144447ceef2ec62b078c27b81fe7 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 16:29:59 +0800 Subject: [PATCH 585/811] Add support for arena mode --- gui/mod-manager.lua | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 832a2c1482..7783fbca4a 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -19,7 +19,7 @@ function vanilla(dir) return dir:startswith('data/vanilla') end --- get_moddable_viewscreen(), get_any_moddable_viewscreen() and get_modlist_fields are declared +-- get_moddable_viewscreen(), get_any_moddable_viewscreen() and get_modlist_fields are declared -- as global functions so external tools can call them to get the DF mod list function get_moddable_viewscreen(type) local vs = nil @@ -31,10 +31,12 @@ function get_moddable_viewscreen(type) return vs end --- get_newregion_viewscreen and get_modlist_fields are declared as global functions --- so external tools can call them to get the DF mod list -function get_newregion_viewscreen() - return get_moddable_viewscreen('region') +function get_any_moddable_viewscreen() + local vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_regionst, 0) + if not vs then + vs = dfhack.gui.getViewscreenByType(df.viewscreen_new_arenast, 0) + end + return vs end function get_modlist_fields(kind, viewscreen) @@ -157,7 +159,7 @@ ModmanageMenu.ATTRS { } local function save_new_preset(preset_name) - local viewscreen = get_newregion_viewscreen() + local viewscreen = get_any_moddable_viewscreen() local modlist = get_active_modlist(viewscreen) table.insert(presets_file.data, { name = preset_name, modlist = modlist }) presets_file:write() @@ -177,7 +179,7 @@ local function overwrite_preset(idx) return end - local viewscreen = get_newregion_viewscreen() + local viewscreen = get_any_moddable_viewscreen() local modlist = get_active_modlist(viewscreen) presets_file.data[idx].modlist = modlist presets_file:write() @@ -188,7 +190,7 @@ local function load_preset(idx, unset_default_on_failure) return end - local viewscreen = get_newregion_viewscreen() + local viewscreen = get_any_moddable_viewscreen() local modlist = presets_file.data[idx].modlist local failures = swap_modlist(viewscreen, modlist) @@ -225,7 +227,7 @@ local function load_preset(idx, unset_default_on_failure) table.insert(text, NEWLINE) end dialogs.showMessage("Warning", text) -end + end end local function find_preset_by_name(name) @@ -593,7 +595,7 @@ ModmanageOverlay.ATTRS { desc = "Adds a link to the mod selection screen for accessing the mod manager.", default_pos = { x=5, y=-6 }, version = 2, - viewscreens = { "new_region/Mods" }, + viewscreens = { "new_region/Mods", "new_arena/Mods" }, default_enabled=true, } @@ -656,7 +658,7 @@ notification_timer_fn() local default_applied = false dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc == SC_VIEWSCREEN_CHANGED then - local vs = get_newregion_viewscreen() + local vs = get_any_moddable_viewscreen() if vs and not default_applied then default_applied = true for i, v in ipairs(presets_file.data) do From d6e12541a4c88b39287f3c325d991aaae92cace6 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 18:12:00 +0800 Subject: [PATCH 586/811] Add notifications for updated mods --- gui/mod-manager.lua | 99 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 75 insertions(+), 24 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 7783fbca4a..16f6aa037d 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -13,7 +13,7 @@ local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' -- Shamelessly taken from hack/library/lua/script-manager.lua -function vanilla(dir) +local function vanilla(dir) dir = dir.value dir = dir -- better safe than sorry i guess return dir:startswith('data/vanilla') @@ -75,23 +75,29 @@ function get_modlist_fields(kind, viewscreen) end end +--- @return { success: boolean, version: string } local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) local from_fields = get_modlist_fields(from, viewscreen) local mod_index = nil + local loaded_version = nil for i, v in ipairs(from_fields.id) do local version = from_fields.numeric_version[i] local src_dir = from_fields.src_dir[i] + local displayed_version = from_fields.displayed_version[i].value -- assumes that vanilla mods will not have multiple possible indices. if v.value == mod_id and (vanilla(src_dir) or version == mod_version) then + if version ~= mod_version then + loaded_version = displayed_version + end mod_index = i break end end if mod_index == nil then - return false + return { success= false, version= nil } end for k, v in pairs(to_fields) do @@ -106,13 +112,15 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) v:erase(mod_index) end - return true + return { success= true, version= loaded_version } end +--- @return { success: boolean, version: string } local function enable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) end +--- @return { success: boolean, version: string } local function disable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) end @@ -127,6 +135,7 @@ local function get_active_modlist(viewscreen) return t end +--- @return { failures: [string], changed: [{ id: string, new: string }] } local function swap_modlist(viewscreen, modlist) local current = get_active_modlist(viewscreen) for _, v in ipairs(current) do @@ -134,12 +143,17 @@ local function swap_modlist(viewscreen, modlist) end local failures = {} + local changed = {} for _, v in ipairs(modlist) do - if not enable_mod(viewscreen, v.id, v.version) then + res = enable_mod(viewscreen, v.id, v.version) + if not res.success then table.insert(failures, v.id) end + if res.version then + table.insert(changed, { id= v.id, new= res.version }) + end end - return failures + return { failures= failures, changed= changed } end -------------------- @@ -192,33 +206,53 @@ local function load_preset(idx, unset_default_on_failure) local viewscreen = get_any_moddable_viewscreen() local modlist = presets_file.data[idx].modlist - local failures = swap_modlist(viewscreen, modlist) - - if #failures > 0 then - local text = {} - if unset_default_on_failure then - presets_file.data[idx].default = false - presets_file:write() - - table.insert(text, { - text='Failed to load some mods from your default preset.', - pen=COLOR_LIGHTRED, - }) + local results = swap_modlist(viewscreen, modlist) + local failures = results.failures + local changes = results.changed + local text = {} + + local failed = #failures > 0 + local changed = #changes > 0 + local should_warn = failed or changed + + if should_warn then + if failed then + if unset_default_on_failure then + presets_file.data[idx].default = false + presets_file:write() + + table.insert(text, { + text='Failed to load some mods from your default preset.', + pen=COLOR_LIGHTRED, + }) + table.insert(text, NEWLINE) + table.insert(text, { + text='Preset is being unmarked as the default for safety.', + pen=COLOR_LIGHTRED, + }) + else + table.insert(text, { + text='Failed to load some mods from the preset.', + pen=COLOR_LIGHTRED, + }) + end + end + if failed and changed then table.insert(text, NEWLINE) + end + if changed then table.insert(text, { - text='Preset is being unmarked as the default for safety.', - pen=COLOR_LIGHTRED, - }) - else - table.insert(text, { - text='Failed to load some mods from the preset.', + text='Some vanilla mods have been updated.', pen=COLOR_LIGHTRED, }) end table.insert(text, NEWLINE) - table.insert(text, NEWLINE) table.insert(text, 'Please re-create your preset with mods you currently have installed.') table.insert(text, NEWLINE) + table.insert(text, NEWLINE) + end + + if failed then table.insert(text, 'Here are the mods that failed to load:') table.insert(text, NEWLINE) table.insert(text, NEWLINE) @@ -226,6 +260,23 @@ local function load_preset(idx, unset_default_on_failure) table.insert(text, ('- %s'):format(v)) table.insert(text, NEWLINE) end + end + + if failed and changed then + table.insert(text, NEWLINE) -- just to separate the sections + end + + if changed then + table.insert(text, 'Here are the vanilla mods that have been updated:') + table.insert(text, NEWLINE) + table.insert(text, NEWLINE) + for _, v in ipairs(changes) do + table.insert(text, ('- %s to %s'):format(v.id, v.new)) + table.insert(text, NEWLINE) + end + end + + if should_warn then dialogs.showMessage("Warning", text) end end From a292f126b17d133c97e66b6ccd143b90f924511e Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 18:25:48 +0800 Subject: [PATCH 587/811] Update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index a937e2fb20..afab6089d7 100644 --- a/changelog.txt +++ b/changelog.txt @@ -35,6 +35,7 @@ Template for new versions: ## Fixes - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files +- `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset - `uniform-unstick`: resolve overlap with new buttons in 51.13 ## Misc Improvements From 90d437e9a7adad61ec8139df3e730345037d6bdd Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 19:56:56 +0800 Subject: [PATCH 588/811] Refactor warning logic --- gui/mod-manager.lua | 82 ++++++++++++++++++++++++--------------------- 1 file changed, 44 insertions(+), 38 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 16f6aa037d..0c53078ec8 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -199,6 +199,44 @@ local function overwrite_preset(idx) presets_file:write() end +local function prepare_warning(text, failed, changed, unset_default_on_failure) + if not failed and not changed then return end + + if failed then + if unset_default_on_failure then + table.insert(text, { + text='Failed to load some mods from your default preset.', + pen=COLOR_LIGHTRED, + }) + table.insert(text, NEWLINE) + table.insert(text, { + text='Preset is being unmarked as the default for safety.', + pen=COLOR_LIGHTRED, + }) + else + table.insert(text, { + text='Failed to load some mods from the preset.', + pen=COLOR_LIGHTRED, + }) + end + end + + if failed and changed then + table.insert(text, NEWLINE) + end + + if changed then + table.insert(text, { + text='Some vanilla mods have been updated.', + pen=COLOR_LIGHTRED, + }) + end + table.insert(text, NEWLINE) + table.insert(text, 'Please re-create your preset with mods you currently have installed.') + table.insert(text, NEWLINE) + table.insert(text, NEWLINE) +end + local function load_preset(idx, unset_default_on_failure) if idx > #presets_file.data then return @@ -213,43 +251,11 @@ local function load_preset(idx, unset_default_on_failure) local failed = #failures > 0 local changed = #changes > 0 - local should_warn = failed or changed - - if should_warn then - if failed then - if unset_default_on_failure then - presets_file.data[idx].default = false - presets_file:write() - - table.insert(text, { - text='Failed to load some mods from your default preset.', - pen=COLOR_LIGHTRED, - }) - table.insert(text, NEWLINE) - table.insert(text, { - text='Preset is being unmarked as the default for safety.', - pen=COLOR_LIGHTRED, - }) - else - table.insert(text, { - text='Failed to load some mods from the preset.', - pen=COLOR_LIGHTRED, - }) - end - end - if failed and changed then - table.insert(text, NEWLINE) - end - if changed then - table.insert(text, { - text='Some vanilla mods have been updated.', - pen=COLOR_LIGHTRED, - }) - end - table.insert(text, NEWLINE) - table.insert(text, 'Please re-create your preset with mods you currently have installed.') - table.insert(text, NEWLINE) - table.insert(text, NEWLINE) + + prepare_warning(text, failed, changed) + if failed and unset_default_on_failure then + presets_file.data[idx].default = false + presets_file:write() end if failed then @@ -276,7 +282,7 @@ local function load_preset(idx, unset_default_on_failure) end end - if should_warn then + if failed or changed then dialogs.showMessage("Warning", text) end end From 3655e85492b9e0fd7a0ae27814d5861a6f6788dd Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 23:11:49 +0800 Subject: [PATCH 589/811] Remove excess locals assignment --- gui/mod-manager.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 0c53078ec8..686fc31f29 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -15,7 +15,6 @@ local GLOBAL_KEY = 'mod-manager' -- Shamelessly taken from hack/library/lua/script-manager.lua local function vanilla(dir) dir = dir.value - dir = dir -- better safe than sorry i guess return dir:startswith('data/vanilla') end From 3612f48ed1f749f50aa462d196c940b7982eeb4e Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 23:22:54 +0800 Subject: [PATCH 590/811] Add missing local assignment --- gui/mod-manager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 686fc31f29..350ac3143c 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -144,7 +144,7 @@ local function swap_modlist(viewscreen, modlist) local failures = {} local changed = {} for _, v in ipairs(modlist) do - res = enable_mod(viewscreen, v.id, v.version) + local res = enable_mod(viewscreen, v.id, v.version) if not res.success then table.insert(failures, v.id) end From 71c5b15fea8979aa1937c6eaff36c50c66d54070 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 13 Jul 2025 23:58:29 +0800 Subject: [PATCH 591/811] Add missing changelog entry --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index afab6089d7..e5bd22e76e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,6 +36,7 @@ Template for new versions: - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files - `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset +- `gui/mod-manager`: now supports arena mode - `uniform-unstick`: resolve overlap with new buttons in 51.13 ## Misc Improvements From 86e636f1b4327ab606c27cc36e306258ee809461 Mon Sep 17 00:00:00 2001 From: git--amade Date: Tue, 15 Jul 2025 19:08:38 +0800 Subject: [PATCH 592/811] Add entomb.lua and docs/entomb.rst --- docs/entomb.rst | 61 +++++++++++++++ entomb.lua | 193 ++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 254 insertions(+) create mode 100644 docs/entomb.rst create mode 100644 entomb.lua diff --git a/docs/entomb.rst b/docs/entomb.rst new file mode 100644 index 0000000000..c519ff7bcf --- /dev/null +++ b/docs/entomb.rst @@ -0,0 +1,61 @@ +entomb +====== + +.. dfhack-tool:: + :summary: Entomb any corpse into tomb zones. + :tags: fort items buildings + +Assign any corpse regardless of citizenship, residency, pet status, +or affiliation to an unassigned tomb zone for burial. + +Usage +----- + +``entomb []`` + +This script must be executed with either a unit's corpse or body part +selected or with a unit ID specified. An unassigned tomb zone will then +be assigned to the unit for burial and all its corpse and/or body parts +will become valid items for interment. + +Optionally, the zone ID may also be specified to assign a specific tomb +zone to the unit. + +A non-citizen, non-resident, or non-pet unit that is still alive may +even be assigned a tomb zone if they have lost any body part that can +be placed inside a tomb, e.g. teeth or severed limbs. New corpse items +after a tomb has already been assigned will not be properly interred +until the script is executed again on either the unit, its corpse, or +any of its body parts. + +If executed on slaughtered animals, all its butchering returns will +become valid burial items and no longer usable for cooking or crafting. + +Examples +-------- + +``entomb unit `` + Assign an unassigned tomb zone to the unit with the specified ID. + +``entomb tomb `` + Assign a tomb zone with the specified ID to the selected corpse + item's unit. + +``entomb unit tomb now`` + Assign a tomb zone with the specified ID to the unit with the + specified ID and teleport its corpse and/or body parts into the + coffin in the tomb zone. + +Options +------- + +``unit `` + Specify the ID of the unit to be assigned to a tomb zone. + +``tomb `` + Specify the ID of the zone into which a unit will be interred. + +``now`` + Instantly teleport the unit's corpse and/or body parts into the + coffin of its assigned tomb zone. This option can be called on + corpse items or units that are already assigned a tomb zone. diff --git a/entomb.lua b/entomb.lua new file mode 100644 index 0000000000..2780232d25 --- /dev/null +++ b/entomb.lua @@ -0,0 +1,193 @@ +-- Entomb corpse items of any dead unit. +--@module = true + +local utils = require('utils') + +local unit_id +local unit +local building_id +local tomb +local forceBurial + +local args = {...} + +-- Get unit from selected corpse or corpse piece item. +local function GetUnitFromCorpse() + local item = dfhack.gui.getSelectedItem(true) + if item then + if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then + unit_id = item.unit_id + unit = df.unit.find(unit_id) + else + qerror('Selected item is not a corpse or body part.') + end + else + qerror('No item selected or unit specified.') + end +end + +-- Validate tomb zone assignment. +local function CheckTombZone(building, id) + if df.building_civzonest:is_instance(building) then + if building.type == 97 then + if building.assigned_unit_id == id then + return true + end + end + end +end + +-- Iterate through all available tomb zones. +local function IterateTombZone(id) + for _, building in pairs(df.global.world.buildings.all) do + if CheckTombZone(building, id) then return building end + end +end + +-- Check if any of the unit's corpse items are still not in a coffin. +local function isNotBuried() + for _, item_id in pairs(unit.corpse_parts) do + local item = df.item.find(item_id) + if item then + local inCoffin = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) + local coffinBuilding_id = inCoffin and inCoffin.building_id or nil + local coffin = coffinBuilding_id and df.building.find(coffinBuilding_id) or nil + local isCoffin = coffin and df.building_coffinst:is_instance(coffin) or nil + -- Return TRUE if even one item is not interred. + if not isCoffin then + return true + end + end + end +end + +local function GetEmptyTombZone() + -- Check if unit is already assigned to a tomb zone. + local isAlreadyAssigned = IterateTombZone(unit_id) + if isAlreadyAssigned then + if isNotBuried() or forceBurial then + tomb = isAlreadyAssigned + print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') + else + qerror('Unit is already interred in a tomb zone.') + end + else + -- Find an unassigned tomb zone. + tomb = IterateTombZone(-1) + end + if not tomb then + qerror('No unassigned tomb zones are available.') + end +end + +-- Set corpse items to be valid for burial. +local function FlagForBurial(corpseParts) + -- Undead units have empty corpse_parts vector. + if unit.enemy.undead then + for _, item in pairs(df.global.world.items.other.IN_PLAY) do + if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then + if item.unit_id == unit_id then + corpseParts:insert(#corpseParts, item.id) + end + end + end + utils.sort_vector(corpseParts) + end + local burialItemCount = 0 + for _, item_id in pairs(corpseParts) do + local item = df.item.find(item_id) + if item then + item.flags.dead_dwarf = true + -- Some corpse items may be lost/destroyed before burial. + burialItemCount = burialItemCount + 1 + end + end + if burialItemCount == 0 then + qerror('Unit has no corpse or body parts available for burial.') + end + tomb.assigned_unit_id = unit_id + return burialItemCount +end + +local function PutInCoffin(corpseParts) + local coffin + for _, building in pairs(tomb.contained_buildings) do + if df.building_coffinst:is_instance(building) then coffin = building end + end + if coffin then + -- Set df.building_item_role_type.PERM first before changing + -- it to TEMP to turn it into an interred corpse item. + for _, item_id in pairs(corpseParts) do + local item = df.item.find(item_id) + if item then + dfhack.items.moveToBuilding(item, coffin, 2) + end + end + for _, buildingItem in pairs(coffin.contained_items) do + local item = buildingItem.item + if not df.item_coffinst:is_instance(item) then + buildingItem.use_mode = 0 + end + end + print('Corpse items have been teleported into a coffin.') + else + print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') + end +end + +local function AssignToTomb() + local corpseParts = unit.corpse_parts + local strBurial = '%s assigned to a tomb zone for burial.' + local strCorpseItems = '(%d corpse or body part%s)' + local strUnitName = unit and dfhack.units.getReadableName(unit) + local strPlural = '' + local incident_id = unit.counters.death_id + if incident_id ~= -1 then + local incident = df.incident.find(incident_id) + -- Corpse will not be interred if not yet discovered. + incident.flags.discovered = true + end + local burialItemCount = FlagForBurial(corpseParts) + print(string.format(strBurial, strUnitName)) + if forceBurial then PutInCoffin(corpseParts) end + if burialItemCount > 1 then strPlural = 's' end + print(string.format(strCorpseItems, burialItemCount, strPlural)) +end + +local function parseArgs() + local building + if #args > 0 then + for i, v in ipairs(args) do + if v == 'unit' then + unit_id = tonumber(args[i+1]) or nil + unit = unit_id and df.unit.find(unit_id) + if not unit then qerror('Invalid unit ID.') end + end + if v == 'tomb' then + building_id = tonumber(args[i+1]) or nil + building = building_id and df.building.find(building_id) + if not building then qerror('Invalid zone ID.') end + -- Check if tomb zone is unassigned. + if CheckTombZone(building, -1) then + tomb = building + else + qerror('Specified zone ID does not point to an unassigned tomb zone.') + end + end + if v == 'now' then forceBurial = true end + end + end +end + +local function Main() + parseArgs() + if not unit then GetUnitFromCorpse() end + if unit then + if not tomb then GetEmptyTombZone() end + if tomb then AssignToTomb() end + end +end + +if not dfhack_flags.module then + Main() +end From f684542ccc42068a4785d9c9f296467be87d0521 Mon Sep 17 00:00:00 2001 From: git--amade Date: Tue, 15 Jul 2025 19:26:31 +0800 Subject: [PATCH 593/811] Update changelog.txt to add new tool: entomb --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index a937e2fb20..624dc7723f 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,6 +29,7 @@ Template for new versions: # Future ## New Tools +- `entomb`: allow any unit that has a corpse or body parts to be assigned a tomb zone ## New Features From 07594b542d594c9201c2332167cabeb087865a32 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 16 Jul 2025 21:31:53 +0800 Subject: [PATCH 594/811] Apply revisions to entomb.lua according to PR comments --- entomb.lua | 206 ++++++++++++++++++++++++++++------------------------- 1 file changed, 109 insertions(+), 97 deletions(-) diff --git a/entomb.lua b/entomb.lua index 2780232d25..e462456d4c 100644 --- a/entomb.lua +++ b/entomb.lua @@ -1,100 +1,85 @@ -- Entomb corpse items of any dead unit. --@module = true -local utils = require('utils') - -local unit_id -local unit -local building_id -local tomb -local forceBurial - -local args = {...} - -- Get unit from selected corpse or corpse piece item. -local function GetUnitFromCorpse() - local item = dfhack.gui.getSelectedItem(true) +function GetUnitFromCorpse(item) + if math.type(item) == "integer" then item = df.item.find(item) + elseif not item then item = dfhack.gui.getSelectedItem(true) end if item then if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then - unit_id = item.unit_id - unit = df.unit.find(unit_id) + return df.unit.find(item.unit_id) else - qerror('Selected item is not a corpse or body part.') + qerror('Item is not a corpse or body part.') end - else - qerror('No item selected or unit specified.') end end -- Validate tomb zone assignment. -local function CheckTombZone(building, id) - if df.building_civzonest:is_instance(building) then - if building.type == 97 then - if building.assigned_unit_id == id then - return true - end +local function CheckTombZone(building, unit_id) + if building.type == df.civzone_type.Tomb then + if building.assigned_unit_id == unit_id then + return true end end end -- Iterate through all available tomb zones. -local function IterateTombZone(id) - for _, building in pairs(df.global.world.buildings.all) do - if CheckTombZone(building, id) then return building end +local function IterateTombZones(unit_id) + for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do + if CheckTombZone(building, unit_id) then return building end end end --- Check if any of the unit's corpse items are still not in a coffin. -local function isNotBuried() - for _, item_id in pairs(unit.corpse_parts) do +-- Check if any of the unit's corpse items are not yet placed in a coffin. +function isEntombed(unit) + -- Return FALSE for still living or undead units with empty corpse_parts vector. + if #unit.corpse_parts == 0 then return false end + for _, item_id in ipairs(unit.corpse_parts) do local item = df.item.find(item_id) if item then - local inCoffin = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) - local coffinBuilding_id = inCoffin and inCoffin.building_id or nil - local coffin = coffinBuilding_id and df.building.find(coffinBuilding_id) or nil - local isCoffin = coffin and df.building_coffinst:is_instance(coffin) or nil - -- Return TRUE if even one item is not interred. + local inBuilding = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) + local building_id = inBuilding and inBuilding.building_id or -1 + local building = df.building.find(building_id) + local isCoffin = (building and df.building_coffinst:is_instance(building)) or false + -- Return FALSE if even one item is not interred. if not isCoffin then - return true + return false end end end + return true end -local function GetEmptyTombZone() +local function GetTombZone(unit) + local unit_id = unit.id + local tomb + local entombed = false -- Check if unit is already assigned to a tomb zone. - local isAlreadyAssigned = IterateTombZone(unit_id) + local isAlreadyAssigned = IterateTombZones(unit_id) if isAlreadyAssigned then - if isNotBuried() or forceBurial then - tomb = isAlreadyAssigned - print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') - else - qerror('Unit is already interred in a tomb zone.') - end + tomb = isAlreadyAssigned + entombed = isEntombed(unit) else -- Find an unassigned tomb zone. - tomb = IterateTombZone(-1) - end - if not tomb then - qerror('No unassigned tomb zones are available.') + tomb = IterateTombZones(-1) end + return tomb, entombed end -- Set corpse items to be valid for burial. -local function FlagForBurial(corpseParts) +local function FlagForBurial(unit, corpseParts) -- Undead units have empty corpse_parts vector. if unit.enemy.undead then - for _, item in pairs(df.global.world.items.other.IN_PLAY) do + for _, item in ipairs(df.global.world.items.other.ANY_CORPSE) do if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then - if item.unit_id == unit_id then - corpseParts:insert(#corpseParts, item.id) + if item.unit_id == unit.id then + corpseParts:insert('#', item.id) end end end - utils.sort_vector(corpseParts) end local burialItemCount = 0 - for _, item_id in pairs(corpseParts) do + for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) if item then item.flags.dead_dwarf = true @@ -102,70 +87,87 @@ local function FlagForBurial(corpseParts) burialItemCount = burialItemCount + 1 end end - if burialItemCount == 0 then - qerror('Unit has no corpse or body parts available for burial.') - end - tomb.assigned_unit_id = unit_id return burialItemCount end -local function PutInCoffin(corpseParts) - local coffin - for _, building in pairs(tomb.contained_buildings) do - if df.building_coffinst:is_instance(building) then coffin = building end - end - if coffin then - -- Set df.building_item_role_type.PERM first before changing - -- it to TEMP to turn it into an interred corpse item. - for _, item_id in pairs(corpseParts) do - local item = df.item.find(item_id) - if item then - dfhack.items.moveToBuilding(item, coffin, 2) - end +function PutInCoffin(coffin, item) + if item then + -- Set df.building_item_role_type.PERM first before changing it to TEMP to turn the items + -- into interred burial items, otherwise the items will be hauled back to stockpiles. + -- https://discord.com/channels/793331351645323264/873014631315148840/1394242351345434654 + dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.PERM) end - for _, buildingItem in pairs(coffin.contained_items) do - local item = buildingItem.item - if not df.item_coffinst:is_instance(item) then - buildingItem.use_mode = 0 - end + for _, buildingItem in ipairs(coffin.contained_items) do + local item = buildingItem.item + if not df.item_coffinst:is_instance(item) then + buildingItem.use_mode = df.building_item_role_type.TEMP end - print('Corpse items have been teleported into a coffin.') - else - print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') end end -local function AssignToTomb() +local function GetCoffin(tomb) + local coffin + if tomb.type == df.civzone_type.Tomb then + for _, building in ipairs(tomb.contained_buildings) do + if df.building_coffinst:is_instance(building) then coffin = building end + end + -- Allow other scripts to call this function and pass the actual coffin building instead. + elseif df.building_coffinst:is_instance(tomb) then + coffin = tomb + end + return coffin +end + +function AssignToTomb(unit, tomb, forceBurial) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to a tomb zone for burial.' local strCorpseItems = '(%d corpse or body part%s)' + local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) local strPlural = '' local incident_id = unit.counters.death_id if incident_id ~= -1 then local incident = df.incident.find(incident_id) - -- Corpse will not be interred if not yet discovered. + -- Corpse will not be interred if not yet discovered, + -- which never happens for units not belonging to player's civ. incident.flags.discovered = true end - local burialItemCount = FlagForBurial(corpseParts) - print(string.format(strBurial, strUnitName)) - if forceBurial then PutInCoffin(corpseParts) end - if burialItemCount > 1 then strPlural = 's' end - print(string.format(strCorpseItems, burialItemCount, strPlural)) + local burialItemCount = FlagForBurial(unit, corpseParts) + if burialItemCount == 0 then + print(string.format(strNoCorpse, strUnitName)) + else + tomb.assigned_unit_id = unit.id + print(string.format(strBurial, strUnitName)) + if forceBurial then + local coffin = GetCoffin(tomb) + print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') + if coffin then + for _, item_id in ipairs(corpseParts) do + local item = df.item.find(item_id) + PutInCoffin(coffin, item) + end + print('Corpse items have been teleported into a coffin.') + else + print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') + end + end + if burialItemCount > 1 then strPlural = 's' end + print(string.format(strCorpseItems, burialItemCount, strPlural)) + end end -local function parseArgs() - local building - if #args > 0 then +local function parseArgs(args) + local unit, tomb, forceBurial + if args and #args > 0 then for i, v in ipairs(args) do if v == 'unit' then - unit_id = tonumber(args[i+1]) or nil + local unit_id = tonumber(args[i+1]) or nil unit = unit_id and df.unit.find(unit_id) if not unit then qerror('Invalid unit ID.') end end if v == 'tomb' then - building_id = tonumber(args[i+1]) or nil - building = building_id and df.building.find(building_id) + local building_id = tonumber(args[i+1]) or nil + local building = building_id and df.building.find(building_id) if not building then qerror('Invalid zone ID.') end -- Check if tomb zone is unassigned. if CheckTombZone(building, -1) then @@ -177,17 +179,27 @@ local function parseArgs() if v == 'now' then forceBurial = true end end end + return unit, tomb, forceBurial end -local function Main() - parseArgs() - if not unit then GetUnitFromCorpse() end +local function Main(args) + local unit, tomb, forceBurial = parseArgs(args) + local entombed + if not unit then unit = GetUnitFromCorpse() end if unit then - if not tomb then GetEmptyTombZone() end - if tomb then AssignToTomb() end + if not tomb then tomb, entombed = GetTombZone(unit) end + if entombed then + print('Unit is already completely interred in a tomb zone.') + elseif tomb then + AssignToTomb(unit, tomb, forceBurial) + else + print('No unassigned tomb zones are available.') + end + else + qerror('No item selected or unit specified.') end end if not dfhack_flags.module then - Main() + Main({...}) end From 0f75b5622740e2cfb40ab70e826748598012d1af Mon Sep 17 00:00:00 2001 From: git--amade Date: Thu, 17 Jul 2025 03:31:32 +0800 Subject: [PATCH 595/811] Apply 2nd revision to entomb.lua: improve PutInCoffin() --- entomb.lua | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/entomb.lua b/entomb.lua index e462456d4c..88483d4609 100644 --- a/entomb.lua +++ b/entomb.lua @@ -21,6 +21,7 @@ local function CheckTombZone(building, unit_id) return true end end + return false end -- Iterate through all available tomb zones. @@ -28,6 +29,7 @@ local function IterateTombZones(unit_id) for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do if CheckTombZone(building, unit_id) then return building end end + return nil end -- Check if any of the unit's corpse items are not yet placed in a coffin. @@ -92,15 +94,17 @@ end function PutInCoffin(coffin, item) if item then - -- Set df.building_item_role_type.PERM first before changing it to TEMP to turn the items - -- into interred burial items, otherwise the items will be hauled back to stockpiles. - -- https://discord.com/channels/793331351645323264/873014631315148840/1394242351345434654 - dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.PERM) + -- Remove job from item to allow it to be teleported. + if item.flags.in_job then + local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + local job = inJob and inJob.data.job + if job then + dfhack.job.removeJob(job) + end end - for _, buildingItem in ipairs(coffin.contained_items) do - local item = buildingItem.item - if not df.item_coffinst:is_instance(item) then - buildingItem.use_mode = df.building_item_role_type.TEMP + if (dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.TEMP)) then + -- Flag the item become an interred item, otherwise it will be hauled back to stockpiles. + item.flags.in_building = true end end end @@ -120,7 +124,9 @@ end function AssignToTomb(unit, tomb, forceBurial) local corpseParts = unit.corpse_parts - local strBurial = '%s assigned to a tomb zone for burial.' + local strBurial = '%s assigned to %s for burial.' + local strTomb = 'a tomb zone' + if #tomb.name > 0 then strTomb = tomb.name end local strCorpseItems = '(%d corpse or body part%s)' local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) @@ -137,10 +143,9 @@ function AssignToTomb(unit, tomb, forceBurial) print(string.format(strNoCorpse, strUnitName)) else tomb.assigned_unit_id = unit.id - print(string.format(strBurial, strUnitName)) + print(string.format(strBurial, strUnitName, strTomb)) if forceBurial then local coffin = GetCoffin(tomb) - print('Unit is already assigned to a tomb zone but may still have uninterred corpse or body part(s).') if coffin then for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) From 16ce689f74a5a58d67391e7516ca42664b7914d6 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Thu, 17 Jul 2025 22:53:03 +0800 Subject: [PATCH 596/811] Fix possible fallthrough in swap_modlist --- gui/mod-manager.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 350ac3143c..87928b5eb1 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -147,8 +147,7 @@ local function swap_modlist(viewscreen, modlist) local res = enable_mod(viewscreen, v.id, v.version) if not res.success then table.insert(failures, v.id) - end - if res.version then + elseif res.version then table.insert(changed, { id= v.id, new= res.version }) end end From 349321c9f166670e317d53b7bcdfe5541493da1c Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 17 Jul 2025 14:10:39 -0500 Subject: [PATCH 597/811] correct typo in export-dt-ini.lua --- devel/export-dt-ini.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/devel/export-dt-ini.lua b/devel/export-dt-ini.lua index 84e1da5cf6..5bf5eadd6c 100644 --- a/devel/export-dt-ini.lua +++ b/devel/export-dt-ini.lua @@ -318,7 +318,7 @@ address('physical_attrs',df.unit,'body','physical_attrs') address('body_size',df.unit,'appearance','body_modifiers') address('size_info',df.unit,'body','size_info','size_cur') address('size_base',df.unit,'body','size_info','size_base') -address('curse',df.unit,'uwss_display_name_string') +address('curse',df.unit,'uwss_display_name_sing') address('curse_add_flags1',df.unit,'uwss_add_caste_flag') address('turn_count',df.unit,'usable_interaction','time_on_site') address('souls',df.unit,'status','souls') From 36b7771fab7892e5ed74ef39ee8438657e84904c Mon Sep 17 00:00:00 2001 From: SilasD Date: Fri, 18 Jul 2025 04:24:29 -0700 Subject: [PATCH 598/811] entomb.lua Implement a function that creates a job to haul an item to a coffin. Only the function has been created; the program's UI has not been updated. --- entomb.lua | 49 ++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 48 insertions(+), 1 deletion(-) diff --git a/entomb.lua b/entomb.lua index 88483d4609..8d4db14337 100644 --- a/entomb.lua +++ b/entomb.lua @@ -1,5 +1,6 @@ -- Entomb corpse items of any dead unit. --@module = true +local utils = require('utils') -- Get unit from selected corpse or corpse piece item. function GetUnitFromCorpse(item) @@ -109,6 +110,51 @@ function PutInCoffin(coffin, item) end end +function HaulToCoffin(tomb, coffin, item) + if not tomb or not coffin or not item then return end + + if dfhack.items.getHolderBuilding(item) == coffin and item.flags.in_building == true then +print("DEBUG: item is already properly interred, skipping", tomb.id, dfhack.buildings.getName(tomb), +coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item)) + return -- already interred in this coffin, skip + end + + -- TODO Consider what should happen when certain item.flags are set, particularly .forbid and .dump. + -- TODO Consider copy-paste-modify scripts/internal/caravan/pedestal.lua::is_displayable_item() + + -- Remove current job from item to allow it to be moved to the tomb. + if item.flags.in_job then + local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + local job = inJob and inJob.data.job or nil + if job + and job.job_type == df.job_type.PlaceItemInTomb + and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER) ~= nil + and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER).building_id == tomb.id + then +print("DEBUG: desired job already exists, skipping", tomb.id, dfhack.buildings.getName(tomb), +coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item), job.id) + return -- desired job already exists, skip + end + if job then +print("DEBUG: removing current job from this item", item.id, dfhack.items.getReadableDescription(item), +job.id, df.job_type[job.job_type]) + dfhack.job.removeJob(job) + end + end + + local pos = utils.getBuildingCenter(coffin) + + local job = df.job:new() + job.job_type = df.job_type.PlaceItemInTomb + job.pos = pos + + dfhack.job.attachJobItem(job, item, df.job_role_type.Hauled, -1, -1) + dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, tomb.id) + tomb.jobs:insert('#', job) + + dfhack.job.linkIntoWorld(job, true) +end + local function GetCoffin(tomb) local coffin if tomb.type == df.civzone_type.Tomb then @@ -149,7 +195,8 @@ function AssignToTomb(unit, tomb, forceBurial) if coffin then for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) - PutInCoffin(coffin, item) + -- PutInCoffin(coffin, item) + HaulToCoffin(tomb, coffin, item) end print('Corpse items have been teleported into a coffin.') else From b359cbaa4c7ea3b62da9ccea24ca3fdf6e025581 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 19 Jul 2025 10:31:32 -0700 Subject: [PATCH 599/811] entomb.lua Very Important Bugfix completely assign unit to tomb. --- entomb.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/entomb.lua b/entomb.lua index 8d4db14337..f5e1895d57 100644 --- a/entomb.lua +++ b/entomb.lua @@ -189,6 +189,7 @@ function AssignToTomb(unit, tomb, forceBurial) print(string.format(strNoCorpse, strUnitName)) else tomb.assigned_unit_id = unit.id + tomb.assigned_unit = unit print(string.format(strBurial, strUnitName, strTomb)) if forceBurial then local coffin = GetCoffin(tomb) From 6a185fd92433b03c8e912597b3fdc9814f6c92ee Mon Sep 17 00:00:00 2001 From: SilasD Date: Sat, 19 Jul 2025 21:09:30 -0700 Subject: [PATCH 600/811] entomb.lua Update the unit's owned buildings with the newly-assigned tomb. --- entomb.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/entomb.lua b/entomb.lua index f5e1895d57..0f0e084282 100644 --- a/entomb.lua +++ b/entomb.lua @@ -190,6 +190,9 @@ function AssignToTomb(unit, tomb, forceBurial) else tomb.assigned_unit_id = unit.id tomb.assigned_unit = unit + if not utils.linear_index(unit.owned_buildings, tomb) then + unit.owned_buildings:insert('#', tomb) + end print(string.format(strBurial, strUnitName, strTomb)) if forceBurial then local coffin = GetCoffin(tomb) From 9a3cf1a1c13c0def58979f0c6b59aa3939af7099 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 22 Jul 2025 12:26:13 -0500 Subject: [PATCH 601/811] Update changelog for 52.01-r1 --- changelog.txt | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index a937e2fb20..71e2a281fd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,8 +17,6 @@ Template for new versions: ## New Features ## Fixes -- `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 -- fixed references to removed ``unit.curse`` compound ## Misc Improvements @@ -33,6 +31,20 @@ Template for new versions: ## New Features ## Fixes + +## Misc Improvements + +## Removed + +# 52.01-r1 + +## New Tools + +## New Features + +## Fixes +- fixed references to removed ``unit.curse`` compound +- `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files - `uniform-unstick`: resolve overlap with new buttons in 51.13 From a2f45484b58a7ada57e3c7ea3f436424a873b744 Mon Sep 17 00:00:00 2001 From: SilasD Date: Wed, 23 Jul 2025 11:20:39 -0700 Subject: [PATCH 602/811] Undo commit b359cbaa4c7ea3b62da9ccea24ca3fdf6e025581 entomb.lua Very Important Bugfix completely assign unit to tomb. Because this field was removed in DF 0.51.11. --- entomb.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/entomb.lua b/entomb.lua index 0f0e084282..182981042f 100644 --- a/entomb.lua +++ b/entomb.lua @@ -189,7 +189,6 @@ function AssignToTomb(unit, tomb, forceBurial) print(string.format(strNoCorpse, strUnitName)) else tomb.assigned_unit_id = unit.id - tomb.assigned_unit = unit if not utils.linear_index(unit.owned_buildings, tomb) then unit.owned_buildings:insert('#', tomb) end From 9d10f0fb11689eba6f0853c01e2c41428c02c1e4 Mon Sep 17 00:00:00 2001 From: SilasD Date: Wed, 23 Jul 2025 15:19:26 -0700 Subject: [PATCH 603/811] embark-anyone.lua Test the current viewscreen to ensure that it is the choose_start_site viewscreen, before trying to use it. This was found while diagnosing Issue #5509, but is not related. Minimal changes to the script. --- embark-anyone.lua | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/embark-anyone.lua b/embark-anyone.lua index 10772e46e1..156fcaaf0a 100644 --- a/embark-anyone.lua +++ b/embark-anyone.lua @@ -3,6 +3,9 @@ local utils = require('utils') function addCivToEmbarkList(info) local viewscreen = dfhack.gui.getDFViewscreen(true) + if viewscreen._type ~= df.viewscreen_choose_start_sitest then + qerror("This script can only be used on the embark screen!") + end viewscreen.start_civ:insert ('#', info.civ) viewscreen.start_civ_nem_num:insert ('#', info.nemeses) @@ -12,16 +15,16 @@ end function embarkAnyone() local viewscreen = dfhack.gui.getDFViewscreen(true) + if viewscreen._type ~= df.viewscreen_choose_start_sitest then + qerror("This script can only be used on the embark screen!") + end + local choices, existing_civs = {}, {} for _,existing_civ in ipairs(viewscreen.start_civ) do existing_civs[existing_civ.id] = true end - if viewscreen._type ~= df.viewscreen_choose_start_sitest then - qerror("This script can only be used on the embark screen!") - end - for i, civ in ipairs (df.global.world.entities.all) do -- Test if entity is a civ if civ.type ~= df.historical_entity_type.Civilization then goto continue end From 5295834dc6625da9650ab65cc32ab47318733b55 Mon Sep 17 00:00:00 2001 From: SilasD Date: Thu, 24 Jul 2025 07:46:04 -0700 Subject: [PATCH 604/811] Test the current viewscreen The bug: if the embark-anyone script is executed on any viewscreen other than the embark viewscreen, it aborts with a stack trace. This bugfix makes it cleanly abort with a reasonably-descriptive error message. This was found while diagnosing Issue #5509, but is not related. Minimal changes to the script. Note on the code change: by moving the function addCivToEmbarkList() inside the function embarkAnyone(), addCivToEmbarkList() cannot execute unless embarkAnyone() has at least passed its safety check. --- embark-anyone.lua | 20 +++++++++----------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/embark-anyone.lua b/embark-anyone.lua index 156fcaaf0a..8239cd332b 100644 --- a/embark-anyone.lua +++ b/embark-anyone.lua @@ -1,19 +1,17 @@ local dialogs = require('gui.dialogs') local utils = require('utils') -function addCivToEmbarkList(info) - local viewscreen = dfhack.gui.getDFViewscreen(true) - if viewscreen._type ~= df.viewscreen_choose_start_sitest then - qerror("This script can only be used on the embark screen!") - end +function embarkAnyone() - viewscreen.start_civ:insert ('#', info.civ) - viewscreen.start_civ_nem_num:insert ('#', info.nemeses) - viewscreen.start_civ_entpop_num:insert ('#', info.pops) - viewscreen.start_civ_site_num:insert ('#', info.sites) -end + function addCivToEmbarkList(info) + local viewscreen = dfhack.gui.getDFViewscreen(true) + + viewscreen.start_civ:insert ('#', info.civ) + viewscreen.start_civ_nem_num:insert ('#', info.nemeses) + viewscreen.start_civ_entpop_num:insert ('#', info.pops) + viewscreen.start_civ_site_num:insert ('#', info.sites) + end -function embarkAnyone() local viewscreen = dfhack.gui.getDFViewscreen(true) if viewscreen._type ~= df.viewscreen_choose_start_sitest then qerror("This script can only be used on the embark screen!") From d7e20aa29402b8321bce89f06b3e239a296350df Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 11:58:42 -0500 Subject: [PATCH 605/811] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 71e2a281fd..b0ca7040df 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ Template for new versions: ## New Features ## Fixes +- ``embark-anyone``: validate viewscreen before using, avoids a crash ## Misc Improvements From 166b2378a95721f5bdf6d7575541772ac420b48a Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 24 Jul 2025 13:30:39 -0500 Subject: [PATCH 606/811] Update changelog for 52.02-r1 --- changelog.txt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index b0ca7040df..9be1e38bbe 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,7 +17,6 @@ Template for new versions: ## New Features ## Fixes -- ``embark-anyone``: validate viewscreen before using, avoids a crash ## Misc Improvements @@ -37,6 +36,19 @@ Template for new versions: ## Removed +# 52.02-r1 + +## New Tools + +## New Features + +## Fixes +- ``embark-anyone``: validate viewscreen before using, avoids a crash + +## Misc Improvements + +## Removed + # 52.01-r1 ## New Tools From 25714bf78b8b54705c50ad47c0f13c6b13207861 Mon Sep 17 00:00:00 2001 From: git--amade Date: Fri, 25 Jul 2025 10:20:28 +0800 Subject: [PATCH 607/811] Add function to validate moving items, separate job removal into own function --- entomb.lua | 158 +++++++++++++++++++++++++++++++++-------------------- 1 file changed, 100 insertions(+), 58 deletions(-) diff --git a/entomb.lua b/entomb.lua index 182981042f..a1e1096618 100644 --- a/entomb.lua +++ b/entomb.lua @@ -17,7 +17,7 @@ end -- Validate tomb zone assignment. local function CheckTombZone(building, unit_id) - if building.type == df.civzone_type.Tomb then + if df.building_civzonest:is_instance(building) and building.type == df.civzone_type.Tomb then if building.assigned_unit_id == unit_id then return true end @@ -93,36 +93,21 @@ local function FlagForBurial(unit, corpseParts) return burialItemCount end -function PutInCoffin(coffin, item) - if item then - -- Remove job from item to allow it to be teleported. - if item.flags.in_job then - local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) - local job = inJob and inJob.data.job - if job then - dfhack.job.removeJob(job) - end - end - if (dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.TEMP)) then - -- Flag the item become an interred item, otherwise it will be hauled back to stockpiles. - item.flags.in_building = true - end - end -end - -function HaulToCoffin(tomb, coffin, item) - if not tomb or not coffin or not item then return end - - if dfhack.items.getHolderBuilding(item) == coffin and item.flags.in_building == true then -print("DEBUG: item is already properly interred, skipping", tomb.id, dfhack.buildings.getName(tomb), -coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item)) - return -- already interred in this coffin, skip +-- Adapted from scripts/internal/caravan/pedestal.lua::is_displayable_item() +-- Allow checks for possible use case of interring of non-corpse items. +local function isMoveableItem(tomb, coffin, item, options) + if not item or + item.flags.hostile or + item.flags.removed or + item.flags.spider_web or + item.flags.construction or + item.flags.encased or + item.flags.trader or + item.flags.owned or + item.flags.on_fire + then + return false end - - -- TODO Consider what should happen when certain item.flags are set, particularly .forbid and .dump. - -- TODO Consider copy-paste-modify scripts/internal/caravan/pedestal.lua::is_displayable_item() - - -- Remove current job from item to allow it to be moved to the tomb. if item.flags.in_job then local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) local job = inJob and inJob.data.job or nil @@ -130,34 +115,68 @@ coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDe and job.job_type == df.job_type.PlaceItemInTomb and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER) ~= nil and dfhack.job.getGeneralRef(job, df.general_ref_type.BUILDING_HOLDER).building_id == tomb.id + -- Allow task to be cancelled if teleporting. + and not options.teleport then -print("DEBUG: desired job already exists, skipping", tomb.id, dfhack.buildings.getName(tomb), -coffin.id, dfhack.buildings.getName(coffin), item.id, dfhack.items.getReadableDescription(item), job.id) - return -- desired job already exists, skip + return false end - if job then -print("DEBUG: removing current job from this item", item.id, dfhack.items.getReadableDescription(item), -job.id, df.job_type[job.job_type]) - dfhack.job.removeJob(job) + elseif item.flags.in_inventory then + local inContainer = dfhack.items.getGeneralRef(item, df.general_ref_type.CONTAINED_IN_ITEM) + if not inContainer then return false end + end + if not dfhack.maps.isTileVisible(xyz2pos(dfhack.items.getPosition(item))) then + return false + end + if item.flags.in_building then + local building = dfhack.items.getHolderBuilding(item) + -- Item is already interred. + if building and building == coffin then return false end + for _, containedItem in ipairs(building.contained_items) do + -- Item is part of a building. + if item == contained_item.item then return false end end end + return true +end - local pos = utils.getBuildingCenter(coffin) +-- Remove job from item to allow for hauling or teleportation. +local function RemoveJob(item) + local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) + local job = inJob and inJob.data.job + if job then dfhack.job.removeJob(job) end +end + +function TeleportToCoffin(tomb, coffin, item) + if not tomb or not coffin then return end + local itemName = item and dfhack.items.getReadableDescription(item) or nil + if item.flags.in_job then RemoveJob(item) end + if (dfhack.items.moveToBuilding(item, coffin, df.building_item_role_type.TEMP)) then + -- Flag the item to become an interred item, otherwise it will be hauled back to stockpiles. + item.flags.in_building = true + local strMove = 'Teleporting %d %s into a coffin.' + print(string.format(strMove, item.id, itemName)) + end +end +function HaulToCoffin(tomb, coffin, item) + if not tomb or not coffin then return end + local itemName = item and dfhack.items.getReadableDescription(item) or nil + if item.flags.in_job then RemoveJob(item) end + local pos = utils.getBuildingCenter(coffin) local job = df.job:new() job.job_type = df.job_type.PlaceItemInTomb job.pos = pos - dfhack.job.attachJobItem(job, item, df.job_role_type.Hauled, -1, -1) dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, tomb.id) tomb.jobs:insert('#', job) - dfhack.job.linkIntoWorld(job, true) + local strMove = 'Tasking %d %s for immediate burial.' + print(string.format(strMove, item.id, itemName)) end -local function GetCoffin(tomb) +function GetCoffin(tomb) local coffin - if tomb.type == df.civzone_type.Tomb then + if df.building_civzonest:is_instance(tomb) and tomb.type == df.civzone_type.Tomb then for _, building in ipairs(tomb.contained_buildings) do if df.building_coffinst:is_instance(building) then coffin = building end end @@ -168,15 +187,15 @@ local function GetCoffin(tomb) return coffin end -function AssignToTomb(unit, tomb, forceBurial) +function AssignToTomb(unit, tomb, options) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to %s for burial.' local strTomb = 'a tomb zone' if #tomb.name > 0 then strTomb = tomb.name end local strCorpseItems = '(%d corpse or body part%s)' + local strPlural = '' local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) - local strPlural = '' local incident_id = unit.counters.death_id if incident_id ~= -1 then local incident = df.incident.find(incident_id) @@ -185,6 +204,7 @@ function AssignToTomb(unit, tomb, forceBurial) incident.flags.discovered = true end local burialItemCount = FlagForBurial(unit, corpseParts) + if burialItemCount > 1 then strPlural = 's' end if burialItemCount == 0 then print(string.format(strNoCorpse, strUnitName)) else @@ -193,28 +213,36 @@ function AssignToTomb(unit, tomb, forceBurial) unit.owned_buildings:insert('#', tomb) end print(string.format(strBurial, strUnitName, strTomb)) - if forceBurial then + print(string.format(strCorpseItems, burialItemCount, strPlural)) + if options.haulNow or options.teleport then local coffin = GetCoffin(tomb) if coffin then for _, item_id in ipairs(corpseParts) do local item = df.item.find(item_id) - -- PutInCoffin(coffin, item) - HaulToCoffin(tomb, coffin, item) + if isMoveableItem(tomb, coffin, item, options) then + if options.teleport then + TeleportToCoffin(tomb, coffin, item) + elseif options.haulNow then + HaulToCoffin(tomb, coffin, item) + end + end end - print('Corpse items have been teleported into a coffin.') else - print('No coffin in the assigned tomb zone.\nCorpse items will not be teleported into the tomb zone.') + print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') end end - if burialItemCount > 1 then strPlural = 's' end - print(string.format(strCorpseItems, burialItemCount, strPlural)) end end -local function parseArgs(args) - local unit, tomb, forceBurial +local function ParseArgs(args) + local unit, tomb + local options = { + haulNow = false, + teleport = false + } if args and #args > 0 then for i, v in ipairs(args) do + if v == 'help' then print(dfhack.script_help()) return end if v == 'unit' then local unit_id = tonumber(args[i+1]) or nil unit = unit_id and df.unit.find(unit_id) @@ -231,22 +259,36 @@ local function parseArgs(args) qerror('Specified zone ID does not point to an unassigned tomb zone.') end end - if v == 'now' then forceBurial = true end + if v == 'now' then options.haulNow = true end + if v == 'teleport' then options.teleport = true end + if options.haulNow and options.teleport then + qerror('Burial items cannot be teleported and tasked for hauling simultaneously.') + end end end - return unit, tomb, forceBurial + return unit, tomb, options end local function Main(args) - local unit, tomb, forceBurial = parseArgs(args) - local entombed + if not dfhack.isSiteLoaded() and not dfhack.world.isFortressMode() then + qerror('This script requires the game to be in fortress mode.') + end + local unit, tomb, options = ParseArgs(args) if not unit then unit = GetUnitFromCorpse() end if unit then + local entombed if not tomb then tomb, entombed = GetTombZone(unit) end if entombed then print('Unit is already completely interred in a tomb zone.') elseif tomb then - AssignToTomb(unit, tomb, forceBurial) + -- Prevent multiple tomb zone assignments when tomb ID is specified in the command line. + -- Iterating through building.assigned_unit_id is probably safer than checking in + -- unit.owned_buildings, as a reference in one does not guarantee a reference in the other. + building = IterateTombZones(unit.id) + if building and tomb ~= building then + qerror('Unit already has an assigned tomb zone.') + end + AssignToTomb(unit, tomb, options) else print('No unassigned tomb zones are available.') end From 7952e132191f312996cbd0662720220622d3703c Mon Sep 17 00:00:00 2001 From: git--amade Date: Fri, 25 Jul 2025 15:51:26 +0800 Subject: [PATCH 608/811] Assign new name to unnamed tombs during assignment --- entomb.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/entomb.lua b/entomb.lua index a1e1096618..f5a00e2680 100644 --- a/entomb.lua +++ b/entomb.lua @@ -190,9 +190,16 @@ end function AssignToTomb(unit, tomb, options) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to %s for burial.' - local strTomb = 'a tomb zone' - if #tomb.name > 0 then strTomb = tomb.name end - local strCorpseItems = '(%d corpse or body part%s)' + local strTomb = 'Tomb %d' + -- Provide the tomb's ID so users can invoke it when interring arbitrary items. + strTomb = string.format(strTomb, tomb.id) + if #tomb.name > 0 then + strTomb = tomb.name + else + -- Assign name to unnamed tombs for easier search/reference. + tomb.name = strTomb + end + local strCorpseItems = '(%d corpse, body part%s, or burial item%s)' local strPlural = '' local strNoCorpse = '%s has no corpse or body parts available for burial.' local strUnitName = unit and dfhack.units.getReadableName(unit) @@ -213,7 +220,7 @@ function AssignToTomb(unit, tomb, options) unit.owned_buildings:insert('#', tomb) end print(string.format(strBurial, strUnitName, strTomb)) - print(string.format(strCorpseItems, burialItemCount, strPlural)) + print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) if options.haulNow or options.teleport then local coffin = GetCoffin(tomb) if coffin then From f484912b8efe47aa733b553186f4e7c3a108745d Mon Sep 17 00:00:00 2001 From: git--amade Date: Fri, 25 Jul 2025 17:20:08 +0800 Subject: [PATCH 609/811] Move logic to call HaulToCoffin() and TeleportToCoffin() into new function --- entomb.lua | 127 ++++++++++++++++++++++++++++------------------------- 1 file changed, 66 insertions(+), 61 deletions(-) diff --git a/entomb.lua b/entomb.lua index f5a00e2680..f9af943936 100644 --- a/entomb.lua +++ b/entomb.lua @@ -93,6 +93,56 @@ local function FlagForBurial(unit, corpseParts) return burialItemCount end +function AssignToTomb(unit, tomb) + local corpseParts = unit.corpse_parts + local strBurial = '%s assigned to %s for burial.' + local strTomb = 'Tomb %d' + -- Provide the tomb's ID so users can invoke it when interring arbitrary items. + strTomb = string.format(strTomb, tomb.id) + if #tomb.name > 0 then + strTomb = tomb.name + else + -- Assign name to unnamed tombs for easier search/reference. + tomb.name = strTomb + end + local strCorpseItems = '(%d corpse, body part%s, or burial item%s)' + local strPlural = '' + local strNoCorpse = '%s has no corpse or body parts available for burial.' + local strUnitName = unit and dfhack.units.getReadableName(unit) + local incident_id = unit.counters.death_id + if incident_id ~= -1 then + local incident = df.incident.find(incident_id) + -- Corpse will not be interred if not yet discovered, + -- which never happens for units not belonging to player's civ. + incident.flags.discovered = true + end + local burialItemCount = FlagForBurial(unit, corpseParts) + if burialItemCount > 1 then strPlural = 's' end + if burialItemCount == 0 then + print(string.format(strNoCorpse, strUnitName)) + else + tomb.assigned_unit_id = unit.id + if not utils.linear_index(unit.owned_buildings, tomb) then + unit.owned_buildings:insert('#', tomb) + end + print(string.format(strBurial, strUnitName, strTomb)) + print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) + end +end + +function GetCoffin(tomb) + local coffin + if df.building_civzonest:is_instance(tomb) and tomb.type == df.civzone_type.Tomb then + for _, building in ipairs(tomb.contained_buildings) do + if df.building_coffinst:is_instance(building) then coffin = building end + end + -- Allow other scripts to call this function and pass the actual coffin building instead. + elseif df.building_coffinst:is_instance(tomb) then + coffin = tomb + end + return coffin +end + -- Adapted from scripts/internal/caravan/pedestal.lua::is_displayable_item() -- Allow checks for possible use case of interring of non-corpse items. local function isMoveableItem(tomb, coffin, item, options) @@ -174,70 +224,22 @@ function HaulToCoffin(tomb, coffin, item) print(string.format(strMove, item.id, itemName)) end -function GetCoffin(tomb) - local coffin - if df.building_civzonest:is_instance(tomb) and tomb.type == df.civzone_type.Tomb then - for _, building in ipairs(tomb.contained_buildings) do - if df.building_coffinst:is_instance(building) then coffin = building end - end - -- Allow other scripts to call this function and pass the actual coffin building instead. - elseif df.building_coffinst:is_instance(tomb) then - coffin = tomb - end - return coffin -end - -function AssignToTomb(unit, tomb, options) +local function InterItems(tomb, unit, options) local corpseParts = unit.corpse_parts - local strBurial = '%s assigned to %s for burial.' - local strTomb = 'Tomb %d' - -- Provide the tomb's ID so users can invoke it when interring arbitrary items. - strTomb = string.format(strTomb, tomb.id) - if #tomb.name > 0 then - strTomb = tomb.name - else - -- Assign name to unnamed tombs for easier search/reference. - tomb.name = strTomb - end - local strCorpseItems = '(%d corpse, body part%s, or burial item%s)' - local strPlural = '' - local strNoCorpse = '%s has no corpse or body parts available for burial.' - local strUnitName = unit and dfhack.units.getReadableName(unit) - local incident_id = unit.counters.death_id - if incident_id ~= -1 then - local incident = df.incident.find(incident_id) - -- Corpse will not be interred if not yet discovered, - -- which never happens for units not belonging to player's civ. - incident.flags.discovered = true - end - local burialItemCount = FlagForBurial(unit, corpseParts) - if burialItemCount > 1 then strPlural = 's' end - if burialItemCount == 0 then - print(string.format(strNoCorpse, strUnitName)) - else - tomb.assigned_unit_id = unit.id - if not utils.linear_index(unit.owned_buildings, tomb) then - unit.owned_buildings:insert('#', tomb) - end - print(string.format(strBurial, strUnitName, strTomb)) - print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) - if options.haulNow or options.teleport then - local coffin = GetCoffin(tomb) - if coffin then - for _, item_id in ipairs(corpseParts) do - local item = df.item.find(item_id) - if isMoveableItem(tomb, coffin, item, options) then - if options.teleport then - TeleportToCoffin(tomb, coffin, item) - elseif options.haulNow then - HaulToCoffin(tomb, coffin, item) - end - end + local coffin = GetCoffin(tomb) + if coffin then + for _, item_id in ipairs(corpseParts) do + local item = df.item.find(item_id) + if isMoveableItem(tomb, coffin, item, options) then + if options.teleport then + TeleportToCoffin(tomb, coffin, item) + elseif options.haulNow then + HaulToCoffin(tomb, coffin, item) end - else - print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') end end + else + print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') end end @@ -295,10 +297,13 @@ local function Main(args) if building and tomb ~= building then qerror('Unit already has an assigned tomb zone.') end - AssignToTomb(unit, tomb, options) + AssignToTomb(unit, tomb) else print('No unassigned tomb zones are available.') end + if options.haulNow or options.teleport then + InterItems(tomb, unit, options) + end else qerror('No item selected or unit specified.') end From 821fef9d916a8bcdd4864a163931749b431114f0 Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:28:42 +0800 Subject: [PATCH 610/811] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 87928b5eb1..3a53c19d6e 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -134,7 +134,8 @@ local function get_active_modlist(viewscreen) return t end ---- @return { failures: [string], changed: [{ id: string, new: string }] } +--- @return string[] +--- @return { id: string, new: string }[] local function swap_modlist(viewscreen, modlist) local current = get_active_modlist(viewscreen) for _, v in ipairs(current) do From 466c058a71c2d8d45825a87a8137f9b6dba3d80a Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:30:01 +0800 Subject: [PATCH 611/811] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 3a53c19d6e..48d9606a8b 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -152,7 +152,7 @@ local function swap_modlist(viewscreen, modlist) table.insert(changed, { id= v.id, new= res.version }) end end - return { failures= failures, changed= changed } + return failures, changed end -------------------- From 077e3c9c045c3a29d8432ce506090cdc1c55f011 Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:30:28 +0800 Subject: [PATCH 612/811] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 48d9606a8b..645df30c7d 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -243,7 +243,7 @@ local function load_preset(idx, unset_default_on_failure) local viewscreen = get_any_moddable_viewscreen() local modlist = presets_file.data[idx].modlist - local results = swap_modlist(viewscreen, modlist) + local failures, changed = swap_modlist(viewscreen, modlist) local failures = results.failures local changes = results.changed local text = {} From 0fb8e3a2d28237f8fb274f147933f891036d8fca Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:30:39 +0800 Subject: [PATCH 613/811] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 645df30c7d..8850e40d43 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -245,7 +245,6 @@ local function load_preset(idx, unset_default_on_failure) local modlist = presets_file.data[idx].modlist local failures, changed = swap_modlist(viewscreen, modlist) local failures = results.failures - local changes = results.changed local text = {} local failed = #failures > 0 From 7127f4b10c1b8315a2f44caf36d431bd456d2bd6 Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:31:34 +0800 Subject: [PATCH 614/811] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 8850e40d43..da844c0252 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -74,7 +74,8 @@ function get_modlist_fields(kind, viewscreen) end end ---- @return { success: boolean, version: string } +---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) local from_fields = get_modlist_fields(from, viewscreen) From fbb2ae021edc0c8017d016edf83198901f1a4fcc Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:32:00 +0800 Subject: [PATCH 615/811] Update gui/mod-manager.lua Co-authored-by: SilasD --- gui/mod-manager.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index da844c0252..ac3ce61484 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -112,7 +112,7 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) v:erase(mod_index) end - return { success= true, version= loaded_version } + return true, loaded_version end --- @return { success: boolean, version: string } From ef7986a68e68f900dfdcc2a2dcb2dba5ade391fe Mon Sep 17 00:00:00 2001 From: Ong Ying Gao <52755148+ong-yinggao98@users.noreply.github.com> Date: Sat, 26 Jul 2025 04:32:45 +0800 Subject: [PATCH 616/811] Apply suggestions from code review Co-authored-by: SilasD --- gui/mod-manager.lua | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index ac3ce61484..9bd1778c4a 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -115,12 +115,14 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) return true, loaded_version end ---- @return { success: boolean, version: string } +---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function enable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) end ---- @return { success: boolean, version: string } +---@return boolean # returns true if the mod entry was moved; returns false if the mod or mod version was not found. +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function disable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) end @@ -146,11 +148,11 @@ local function swap_modlist(viewscreen, modlist) local failures = {} local changed = {} for _, v in ipairs(modlist) do - local res = enable_mod(viewscreen, v.id, v.version) - if not res.success then + local success, version = enable_mod(viewscreen, v.id, v.version) + if not success then table.insert(failures, v.id) - elseif res.version then - table.insert(changed, { id= v.id, new= res.version }) + elseif version then + table.insert(changed, { id= v.id, new= version }) end end return failures, changed @@ -245,7 +247,6 @@ local function load_preset(idx, unset_default_on_failure) local viewscreen = get_any_moddable_viewscreen() local modlist = presets_file.data[idx].modlist local failures, changed = swap_modlist(viewscreen, modlist) - local failures = results.failures local text = {} local failed = #failures > 0 From 68f87f6db80ac224218e78b2317bd9820eab6ee5 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 26 Jul 2025 04:39:04 +0800 Subject: [PATCH 617/811] Remove comment --- gui/mod-manager.lua | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 9bd1778c4a..d78f0bc8f2 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -12,7 +12,6 @@ local widgets = require('gui.widgets') local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' --- Shamelessly taken from hack/library/lua/script-manager.lua local function vanilla(dir) dir = dir.value return dir:startswith('data/vanilla') @@ -75,7 +74,7 @@ function get_modlist_fields(kind, viewscreen) end ---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) local from_fields = get_modlist_fields(from, viewscreen) @@ -116,13 +115,13 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) end ---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function enable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) end ---@return boolean # returns true if the mod entry was moved; returns false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return version # string - DISPLAYED_VERSION from the mod's info.txt local function disable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) end From 72b13db868a280e87da4755e6092911e864bec5d Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 26 Jul 2025 05:39:08 +0800 Subject: [PATCH 618/811] Update for 52.02 paths --- gui/mod-manager.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index d78f0bc8f2..5e258b470c 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -13,7 +13,6 @@ local presets_file = json.open("dfhack-config/mod-manager.json") local GLOBAL_KEY = 'mod-manager' local function vanilla(dir) - dir = dir.value return dir:startswith('data/vanilla') end @@ -245,7 +244,7 @@ local function load_preset(idx, unset_default_on_failure) local viewscreen = get_any_moddable_viewscreen() local modlist = presets_file.data[idx].modlist - local failures, changed = swap_modlist(viewscreen, modlist) + local failures, changes = swap_modlist(viewscreen, modlist) local text = {} local failed = #failures > 0 From 63a64fd83ea96e0233c45f4bc9f88333c0a02d11 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sat, 26 Jul 2025 18:03:49 +0800 Subject: [PATCH 619/811] Edit docstrings --- gui/mod-manager.lua | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 5e258b470c..12029661b6 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -72,8 +72,8 @@ function get_modlist_fields(kind, viewscreen) end end ----@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) local from_fields = get_modlist_fields(from, viewscreen) @@ -95,7 +95,7 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) end if mod_index == nil then - return { success= false, version= nil } + return false, nil end for k, v in pairs(to_fields) do @@ -106,21 +106,21 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) end end - for k, v in pairs(from_fields) do + for _, v in pairs(from_fields) do v:erase(mod_index) end return true, loaded_version end ----@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function enable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) end ----@return boolean # returns true if the mod entry was moved; returns false if the mod or mod version was not found. ----@return version # string - DISPLAYED_VERSION from the mod's info.txt +---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function disable_mod(viewscreen, mod_id, mod_version) return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) end From 3cec9d6f116a7e4246723f0aaeb2dfe6b124a697 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sat, 26 Jul 2025 11:52:50 -0500 Subject: [PATCH 620/811] correct changelog for #1481 --- changelog.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 1094ea67de..2b0db42d71 100644 --- a/changelog.txt +++ b/changelog.txt @@ -29,8 +29,10 @@ Template for new versions: ## New Tools ## New Features +- `gui/mod-manager`: now supports arena mode ## Fixes +- `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset ## Misc Improvements @@ -60,8 +62,6 @@ Template for new versions: - `gui/gm-unit`: remove reference to ``think_counter``, removed in v51.12 - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files -- `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset -- `gui/mod-manager`: now supports arena mode - `uniform-unstick`: resolve overlap with new buttons in 51.13 ## Misc Improvements From 699d0f3ad174fde5e7f09955960cd6c9ee201748 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 27 Jul 2025 16:08:24 +0800 Subject: [PATCH 621/811] Add deduplication logic for gui/mod-manager --- gui/mod-manager.lua | 60 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 47 insertions(+), 13 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 12029661b6..7a52ed8adb 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -74,7 +74,7 @@ end ---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt -local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) +local function copy_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) local from_fields = get_modlist_fields(from, viewscreen) @@ -106,23 +106,51 @@ local function move_mod_entry(viewscreen, to, from, mod_id, mod_version) end end - for _, v in pairs(from_fields) do - v:erase(mod_index) - end - return true, loaded_version end ---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function enable_mod(viewscreen, mod_id, mod_version) - return move_mod_entry(viewscreen, "object_load_order", "available", mod_id, mod_version) + return copy_mod_entry(viewscreen, "object_load_order", "base_available", mod_id, mod_version) end ---@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt -local function disable_mod(viewscreen, mod_id, mod_version) - return move_mod_entry(viewscreen, "available", "object_load_order", mod_id, mod_version) +local function make_available_mod(viewscreen, mod_id, mod_version) + return copy_mod_entry(viewscreen, "available", "base_available", mod_id, mod_version) +end + +local function clear_mods(viewscreen) + local active_modlist = get_modlist_fields('object_load_order', viewscreen) + local avail_modlist = get_modlist_fields('available', viewscreen) + for _, modlist in ipairs({active_modlist, avail_modlist}) do + for _, v in pairs(modlist) do + for i = #v - 1, 0, -1 do + v:erase(i) + end + end + end +end + +local function set_available_mods(viewscreen, loaded) + local base_avail = get_modlist_fields('base_available', viewscreen) + local unused = {} + for i, id in ipairs(base_avail.id) do + local j = utils.linear_index(loaded, id.value) + if j then goto continue end + + local version = base_avail.numeric_version[i] + table.insert(unused, { id= id.value, version= version }) + ::continue:: + end + + for _, v in ipairs(unused) do + local success, _ = make_available_mod(viewscreen, v.id, v.version) + if not success then + dfhack.printerr('failed to show '..v.id..' in available list') + end + end end local function get_active_modlist(viewscreen) @@ -138,21 +166,27 @@ end --- @return string[] --- @return { id: string, new: string }[] local function swap_modlist(viewscreen, modlist) - local current = get_active_modlist(viewscreen) - for _, v in ipairs(current) do - disable_mod(viewscreen, v.id, v.version) - end + clear_mods(viewscreen) local failures = {} local changed = {} + local loaded = {} for _, v in ipairs(modlist) do local success, version = enable_mod(viewscreen, v.id, v.version) if not success then table.insert(failures, v.id) - elseif version then + goto continue + end + + table.insert(loaded, v.id) + if version then table.insert(changed, { id= v.id, new= version }) end + + ::continue:: end + + set_available_mods(viewscreen, loaded) return failures, changed end From f0d1be15f41a4317f69ae0911ef1e63d69a1cc21 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 27 Jul 2025 16:12:34 +0800 Subject: [PATCH 622/811] Update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 1094ea67de..8a95feac9c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,6 +17,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/mod-manager`: hide other versions of loaded mods and unhides them when unloaded ## Misc Improvements From 8321e37d6e9b1e551678f50b7c8b3ad1033689e6 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 27 Jul 2025 16:17:32 +0800 Subject: [PATCH 623/811] Update docstrings in gui/mod-manager --- gui/mod-manager.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 7a52ed8adb..5eac49d0f5 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -72,7 +72,7 @@ function get_modlist_fields(kind, viewscreen) end end ----@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return boolean # true if the mod entry was copied over; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function copy_mod_entry(viewscreen, to, from, mod_id, mod_version) local to_fields = get_modlist_fields(to, viewscreen) @@ -109,13 +109,13 @@ local function copy_mod_entry(viewscreen, to, from, mod_id, mod_version) return true, loaded_version end ----@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return boolean # true if the mod entry was copied over; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function enable_mod(viewscreen, mod_id, mod_version) return copy_mod_entry(viewscreen, "object_load_order", "base_available", mod_id, mod_version) end ----@return boolean # true if the mod entry was moved; false if the mod or mod version was not found. +---@return boolean # true if the mod entry was copied over; false if the mod or mod version was not found. ---@return string|nil # loaded version - DISPLAYED_VERSION from the mod's info.txt local function make_available_mod(viewscreen, mod_id, mod_version) return copy_mod_entry(viewscreen, "available", "base_available", mod_id, mod_version) From aae344e11df950b050e40aa2d857e3b3e9f09a38 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 27 Jul 2025 21:37:32 +0800 Subject: [PATCH 624/811] Update gui/mod-manager --- gui/mod-manager.lua | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 5eac49d0f5..476d8afc7a 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -137,8 +137,7 @@ local function set_available_mods(viewscreen, loaded) local base_avail = get_modlist_fields('base_available', viewscreen) local unused = {} for i, id in ipairs(base_avail.id) do - local j = utils.linear_index(loaded, id.value) - if j then goto continue end + if loaded[id.value] then goto continue end local version = base_avail.numeric_version[i] table.insert(unused, { id= id.value, version= version }) @@ -178,7 +177,7 @@ local function swap_modlist(viewscreen, modlist) goto continue end - table.insert(loaded, v.id) + loaded[v.id] = true if version then table.insert(changed, { id= v.id, new= version }) end From b021bc08626e85eceae8ed7f829c61a683e1ee08 Mon Sep 17 00:00:00 2001 From: yg-ong Date: Sun, 27 Jul 2025 23:35:26 +0800 Subject: [PATCH 625/811] Apply code review suggestions --- changelog.txt | 2 +- gui/mod-manager.lua | 22 +++++++++------------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/changelog.txt b/changelog.txt index 8a95feac9c..6139a0d000 100644 --- a/changelog.txt +++ b/changelog.txt @@ -17,7 +17,6 @@ Template for new versions: ## New Features ## Fixes -- `gui/mod-manager`: hide other versions of loaded mods and unhides them when unloaded ## Misc Improvements @@ -32,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `gui/mod-manager`: hide other versions of loaded mods and unhides them when unloaded ## Misc Improvements diff --git a/gui/mod-manager.lua b/gui/mod-manager.lua index 476d8afc7a..a3a745963d 100644 --- a/gui/mod-manager.lua +++ b/gui/mod-manager.lua @@ -137,11 +137,10 @@ local function set_available_mods(viewscreen, loaded) local base_avail = get_modlist_fields('base_available', viewscreen) local unused = {} for i, id in ipairs(base_avail.id) do - if loaded[id.value] then goto continue end - - local version = base_avail.numeric_version[i] - table.insert(unused, { id= id.value, version= version }) - ::continue:: + if not loaded[id.value] then + local version = base_avail.numeric_version[i] + table.insert(unused, { id= id.value, version= version }) + end end for _, v in ipairs(unused) do @@ -174,15 +173,12 @@ local function swap_modlist(viewscreen, modlist) local success, version = enable_mod(viewscreen, v.id, v.version) if not success then table.insert(failures, v.id) - goto continue - end - - loaded[v.id] = true - if version then - table.insert(changed, { id= v.id, new= version }) + else + if version then + table.insert(changed, { id= v.id, new= version }) + end + loaded[v.id] = true end - - ::continue:: end set_available_mods(viewscreen, loaded) From 30993eef1c6ad88279886b6863ad56bbc82f669b Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 27 Jul 2025 12:22:31 -0500 Subject: [PATCH 626/811] Update changelog for 52.02-r2 --- changelog.txt | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index bcf188da2d..8b79bd7b2d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,12 +28,24 @@ Template for new versions: ## New Tools +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 52.02-r2 + +## New Tools + ## New Features - `gui/mod-manager`: now supports arena mode ## Fixes -- `gui/mod-manager`: hide other versions of loaded mods and unhides them when unloaded - `gui/mod-manager`: gracefully handle vanilla mods with different versions from the user's preset +- `gui/mod-manager`: hide other versions of loaded mods and unhides them when unloaded ## Misc Improvements From 514da4a2aecdbd5e033ce90c78e9dba0847f9be7 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 27 Jul 2025 10:45:41 -0700 Subject: [PATCH 627/811] pedestal.lua bad handling of .displayed_items. --- internal/caravan/pedestal.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index 363160093a..5e469c6815 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -599,8 +599,8 @@ end local function unassign_item(bld, item) if not bld then return end - local _, found, idx = utils.binsearch(bld.displayed_items, item.id) - if found then + local idx, _ = utils.linear_index(bld.displayed_items, item.id) + if idx then bld.displayed_items:erase(idx) end end @@ -628,7 +628,7 @@ local function attach_item(item, display_bld) local ref = df.new(df.general_ref_building_display_furniturest) ref.building_id = display_bld.id item.general_refs:insert('#', ref) - utils.insert_sorted(display_bld.displayed_items, item.id) + display_bld.displayed_items:insert('#', item.id) item.flags.forbid = false item.flags.in_building = false end From 842e82e261d47f881e32405d1b087a03577fa121 Mon Sep 17 00:00:00 2001 From: SilasD Date: Sun, 27 Jul 2025 12:00:03 -0700 Subject: [PATCH 628/811] pedestal.lua clear .in_building flag on unassign --- internal/caravan/pedestal.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index 5e469c6815..dab100ef48 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -602,6 +602,7 @@ local function unassign_item(bld, item) local idx, _ = utils.linear_index(bld.displayed_items, item.id) if idx then bld.displayed_items:erase(idx) + item.flags.in_building = false end end From ba5e5d151555d51599d741a12f29efb4a6673ef4 Mon Sep 17 00:00:00 2001 From: git--amade Date: Mon, 28 Jul 2025 18:21:45 +0800 Subject: [PATCH 629/811] Implement add-item option, revise Main() logic, switch to use argparse module --- entomb.lua | 313 +++++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 232 insertions(+), 81 deletions(-) diff --git a/entomb.lua b/entomb.lua index f9af943936..fa40d7960c 100644 --- a/entomb.lua +++ b/entomb.lua @@ -1,8 +1,31 @@ -- Entomb corpse items of any dead unit. --@module = true + +local argparse = require('argparse') local utils = require('utils') +local guidm = require('gui.dwarfmode') --- Get unit from selected corpse or corpse piece item. +-- Check if any of the unit's corpse items are not yet placed in a coffin. +function isEntombed(unit) + -- Return FALSE for still living or undead units with empty corpse_parts vector. + if #unit.corpse_parts == 0 then return false end + for _, item_id in ipairs(unit.corpse_parts) do + local item = df.item.find(item_id) + if item then + local inBuilding = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) + local building_id = inBuilding and inBuilding.building_id or -1 + local building = df.building.find(building_id) + local isCoffin = (building and df.building_coffinst:is_instance(building)) or false + -- Return FALSE if even one item is not interred. + if not isCoffin then + return false + end + end + end + return true +end + +-- Get unit from selected corpse or body part item. function GetUnitFromCorpse(item) if math.type(item) == "integer" then item = df.item.find(item) elseif not item then item = dfhack.gui.getSelectedItem(true) end @@ -10,9 +33,10 @@ function GetUnitFromCorpse(item) if df.item_corpsest:is_instance(item) or df.item_corpsepiecest:is_instance(item) then return df.unit.find(item.unit_id) else - qerror('Item is not a corpse or body part.') + qerror('Selected item is not a corpse or body part.') end end + return nil end -- Validate tomb zone assignment. @@ -33,43 +57,39 @@ local function IterateTombZones(unit_id) return nil end --- Check if any of the unit's corpse items are not yet placed in a coffin. -function isEntombed(unit) - -- Return FALSE for still living or undead units with empty corpse_parts vector. - if #unit.corpse_parts == 0 then return false end - for _, item_id in ipairs(unit.corpse_parts) do - local item = df.item.find(item_id) - if item then - local inBuilding = dfhack.items.getGeneralRef(item, df.general_ref_type.BUILDING_HOLDER) - local building_id = inBuilding and inBuilding.building_id or -1 - local building = df.building.find(building_id) - local isCoffin = (building and df.building_coffinst:is_instance(building)) or false - -- Return FALSE if even one item is not interred. - if not isCoffin then - return false +-- Use when user inputs coffin building ID instead of tomb zone ID. +function GetTombFromCoffin(building) + if #building.relations > 0 then + for _, v in ipairs(building.relations) do + if df.building_civzonest:is_instance(v) and v.type == df.civzone_type.Tomb then + return v end end end - return true + return nil end -local function GetTombZone(unit) - local unit_id = unit.id - local tomb - local entombed = false - -- Check if unit is already assigned to a tomb zone. - local isAlreadyAssigned = IterateTombZones(unit_id) - if isAlreadyAssigned then - tomb = isAlreadyAssigned - entombed = isEntombed(unit) +function GetTombFromZone(building) + if df.building_civzonest:is_instance(building) and building.type == df.civzone_type.Tomb then + return building + elseif df.building_coffinst:is_instance(building) then + return GetTombFromCoffin(building) + end + return nil +end + +function GetTombFromUnit(unit) + -- Check if unit already has a tomb zone assigned. + local alreadyAssignedTomb = unit and IterateTombZones(unit.id) + if alreadyAssignedTomb then + return alreadyAssignedTomb else - -- Find an unassigned tomb zone. - tomb = IterateTombZones(-1) + -- Get an unassigned tomb zone. + return IterateTombZones(-1) end - return tomb, entombed end --- Set corpse items to be valid for burial. +-- Set unit's corpse items to be valid for burial. local function FlagForBurial(unit, corpseParts) -- Undead units have empty corpse_parts vector. if unit.enemy.undead then @@ -97,7 +117,7 @@ function AssignToTomb(unit, tomb) local corpseParts = unit.corpse_parts local strBurial = '%s assigned to %s for burial.' local strTomb = 'Tomb %d' - -- Provide the tomb's ID so users can invoke it when interring arbitrary items. + -- Provide the tomb's ID so the user can invoke it when interring arbitrary items. strTomb = string.format(strTomb, tomb.id) if #tomb.name > 0 then strTomb = tomb.name @@ -144,9 +164,10 @@ function GetCoffin(tomb) end -- Adapted from scripts/internal/caravan/pedestal.lua::is_displayable_item() --- Allow checks for possible use case of interring of non-corpse items. +-- Allow checks for possible use case of interring arbitrary items. local function isMoveableItem(tomb, coffin, item, options) if not item or + -- Allow forbid/dump/melt designated items to be valid. item.flags.hostile or item.flags.removed or item.flags.spider_web or @@ -154,10 +175,15 @@ local function isMoveableItem(tomb, coffin, item, options) item.flags.encased or item.flags.trader or item.flags.owned or + item.flags.garbage_collect or item.flags.on_fire then return false end + -- Allow user to exclude items by forbidding when adding arbitrary items. + if options.addItem and item.flags.forbid then + return false + end if item.flags.in_job then local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) local job = inJob and inJob.data.job or nil @@ -189,6 +215,81 @@ local function isMoveableItem(tomb, coffin, item, options) return true end +function isAlreadyBurialItem(unit, item) + -- Prevent duplicating unit's own corpse parts in corpse_parts. + for _, v in ipairs(unit.corpse_parts) do + if item.id == v then return true end + end + -- Prevent adding burial items belonging to other units with an assigned tomb. + for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do + if not CheckTombZone(building, -1) then + local otherUnit = df.unit.find(building.assigned_unit_id) + for _, v in ipairs(otherUnit.corpse_parts) do + if item.id == v then return true end + end + end + end + return false +end + +-- Set additional arbitrary items to be valid for burial. +function AddBurialItems(unit, tomb, options) + local coffin = GetCoffin(tomb) + local item = dfhack.gui.getSelectedItem(true) + local cursor = guidm.getCursorPos() + local burialItems = {} + local strAddItem = 'Adding %s for burial with unit.' + local strItemName + local strCannotInter = 'Unable to inter additional item(s);\n ...%s.' + local strNoCoffin = 'no coffin in assigned tomb zone' + local strNotValidItem = 'selected item is not valid for burial' + local strNoCursorItems = 'no items at cursor are valid for burial' + local strNoSelect = 'no item selected and keyboard cursor not enabled' + if not coffin then + print(string.format(strCannotInter, strNoCoffin)) + elseif item then + if isMoveableItem(tomb, coffin, item, options) and + not isAlreadyBurialItem(unit, item) + then + strItemName = item and dfhack.items.getReadableDescription(item) or nil + print(string.format(strAddItem, strItemName)) + table.insert(burialItems, item) + else + print(string.format(strCannotInter, strNotValidItem)) + end + -- Use keyboard cursor to set multiple items for burial. + elseif cursor then + -- Filter items to iterate according to tile block at cursor. + local block = dfhack.maps.getTileBlock(cursor) + for _, blockItem_id in ipairs(block.items) do + local blockItem = df.item.find(blockItem_id) + local x, y, _ = dfhack.items.getPosition(blockItem) + if x == cursor.x and y == cursor.y then + item = blockItem + if isMoveableItem(tomb, coffin, item, options) and + not isAlreadyBurialItem(unit, item) + then + strItemName = item and dfhack.items.getReadableDescription(item) or nil + print(string.format(strAddItem, strItemName)) + table.insert(burialItems, item) + end + end + end + if #burialItems == 0 then + print(string.format(strCannotInter, strNoCursorItems)) + end + else + print(string.format(strCannotInter, strNoSelect)) + end + if #burialItems > 0 then + local corpseParts = unit.corpse_parts + for _, burialItem in ipairs(burialItems) do + burialItem.flags.dead_dwarf = true + corpseParts:insert('#', burialItem.id) + end + end +end + -- Remove job from item to allow for hauling or teleportation. local function RemoveJob(item) local inJob = dfhack.items.getSpecificRef(item, df.specific_ref_type.JOB) @@ -239,73 +340,123 @@ local function InterItems(tomb, unit, options) end end else - print('No coffin in the assigned tomb zone.\nCorpse items will not be moved into the tomb zone.') + print('Unable to move burial item(s);\n ...no coffin in assigned tomb zone.') end end -local function ParseArgs(args) - local unit, tomb - local options = { - haulNow = false, - teleport = false - } - if args and #args > 0 then - for i, v in ipairs(args) do - if v == 'help' then print(dfhack.script_help()) return end - if v == 'unit' then - local unit_id = tonumber(args[i+1]) or nil - unit = unit_id and df.unit.find(unit_id) - if not unit then qerror('Invalid unit ID.') end +-- Process unit and tomb before executing operations. +local function PreOpProcess(unit, building, options) + local tomb = building and GetTombFromZone(building) + local entombed = false + if not options.addItem then + if not unit then + unit = GetUnitFromCorpse() + end + if not tomb then + tomb = GetTombFromUnit(unit) + end + if unit and tomb then + -- Unit has a tomb, but it's not the specified tomb. + if IterateTombZones(unit.id) and tomb ~= IterateTombZones(unit.id) then + qerror('Unit already has an assigned tomb zone.') + -- Specified tomb is not assigned to unit, and specified tomb is not unassigned. + elseif not CheckTombZone(tomb, unit.id) and not CheckTombZone(tomb, -1) then + qerror('Specified tomb zone is already assigned to a different unit.') end - if v == 'tomb' then - local building_id = tonumber(args[i+1]) or nil - local building = building_id and df.building.find(building_id) - if not building then qerror('Invalid zone ID.') end - -- Check if tomb zone is unassigned. - if CheckTombZone(building, -1) then - tomb = building - else - qerror('Specified zone ID does not point to an unassigned tomb zone.') - end + end + if unit then + if not tomb then + qerror('No unassigned tomb zones are available.') + end + entombed = isEntombed(unit) + else + qerror('No item selected or unit specified.') + end + else + -- Either a unit or an assigned tomb zone must be specified when add-item is called, + -- as corpse/body part items cannot be used to assign tomb zones with this option. + local strCannotInter = 'Unable to inter additional item(s);\n ...%s.' + local strNoUnit = 'specified tomb zone is not assigned to a unit' + local strNoTomb = 'specified unit has no assigned tomb zone' + local strWrongPair = 'specified tomb zone is not assigned to specified unit' + local strNotSpecified = 'no assigned tomb zone or unit with assigned tomb zone specified' + if tomb and not unit then + if tomb.assigned_unit_id == -1 then + qerror(string.format(strCannotInter, strNoUnit)) end - if v == 'now' then options.haulNow = true end - if v == 'teleport' then options.teleport = true end - if options.haulNow and options.teleport then - qerror('Burial items cannot be teleported and tasked for hauling simultaneously.') + unit = df.unit.find(tomb.assigned_unit_id) + if not unit then + qerror(string.format(strCannotInter, strNoUnit)) end + elseif unit and not tomb then + tomb = GetTombFromUnit(unit) + if not tomb then + -- Equivalent to having no available unassigned tomb zones, + -- but emphasize on unit having no assigned tomb. + qerror(string.format(strCannotInter, strNoTomb)) + end + elseif tomb and unit then + if not CheckTombZone(tomb, unit.id) and not CheckTombZone(tomb, -1) then + qerror(string.format(strCannotInter, strWrongPair)) + end + else + qerror(string.format(strCannotInter, strNotSpecified)) end end - return unit, tomb, options + return unit, tomb, entombed +end + +local function ParseCommandLine(args) + local unit, building + local options = { + help = false, + addItem = false, + haulNow = false, + teleport = false + } + local positionals = argparse.processArgsGetopt(args, { + {'h', 'help', handler = function() options.help = true end}, + {'u', 'unit', hasArg = true, handler = function(arg) + local unit_id = argparse.positiveInt(arg, 'unit') + unit = unit_id and df.unit.find(unit_id) + if not unit then qerror('Invalid unit ID.') end end + }, + {'t', 'tomb', hasArg = true, handler = function(arg) + local building_id = argparse.positiveInt(arg, 'tomb') + building = building_id and df.building.find(building_id) + if not building then qerror('Invalid zone ID.') end end + }, + {'a', 'add-item', handler = function() options.addItem = true end}, + {'h', 'haul-now', handler = function() options.haulNow = true end}, + {'', 'teleport', handler = function() options.teleport = true end} + }) + return unit, building, options end local function Main(args) if not dfhack.isSiteLoaded() and not dfhack.world.isFortressMode() then qerror('This script requires the game to be in fortress mode.') end - local unit, tomb, options = ParseArgs(args) - if not unit then unit = GetUnitFromCorpse() end - if unit then - local entombed - if not tomb then tomb, entombed = GetTombZone(unit) end - if entombed then - print('Unit is already completely interred in a tomb zone.') - elseif tomb then - -- Prevent multiple tomb zone assignments when tomb ID is specified in the command line. - -- Iterating through building.assigned_unit_id is probably safer than checking in - -- unit.owned_buildings, as a reference in one does not guarantee a reference in the other. - building = IterateTombZones(unit.id) - if building and tomb ~= building then - qerror('Unit already has an assigned tomb zone.') - end - AssignToTomb(unit, tomb) - else - print('No unassigned tomb zones are available.') + local unit, building, options = ParseCommandLine(args) + if args == 'help' or options.help then + print(dfhack.script_help()) + return + end + if options.haulNow and options.teleport then + qerror('Burial items cannot be teleported and tasked for hauling simultaneously.') + end + local tomb, entombed + unit, tomb, entombed = PreOpProcess(unit, building, options) + if entombed then + print('Unit is already completely interred in a tomb zone.') + elseif unit and tomb then + AssignToTomb(unit, tomb) + if options.addItem then + AddBurialItems(unit, tomb, options) end if options.haulNow or options.teleport then InterItems(tomb, unit, options) end - else - qerror('No item selected or unit specified.') end end From e11d9680a59f761224ccf5787fc2cd258ddb08de Mon Sep 17 00:00:00 2001 From: git--amade Date: Mon, 28 Jul 2025 20:14:04 +0800 Subject: [PATCH 630/811] Disable teleport function, update documentation --- docs/entomb.rst | 51 +++++++++++++++++++++++++++---------------------- entomb.lua | 3 ++- 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/docs/entomb.rst b/docs/entomb.rst index c519ff7bcf..64f708a3d0 100644 --- a/docs/entomb.rst +++ b/docs/entomb.rst @@ -5,7 +5,7 @@ entomb :summary: Entomb any corpse into tomb zones. :tags: fort items buildings -Assign any corpse regardless of citizenship, residency, pet status, +Assign any unit regardless of citizenship, residency, pet status, or affiliation to an unassigned tomb zone for burial. Usage @@ -13,20 +13,20 @@ Usage ``entomb []`` -This script must be executed with either a unit's corpse or body part -selected or with a unit ID specified. An unassigned tomb zone will then -be assigned to the unit for burial and all its corpse and/or body parts -will become valid items for interment. +Select a unit's corpse or body part, or specify the unit's ID +when executing this script to assign an unassigned tomb zone to +the unit, and flag the unit's corpse as well as any severed body +parts to become valid items for interment. -Optionally, the zone ID may also be specified to assign a specific tomb +Optionally, specify the tomb zone's ID to assign a specific tomb zone to the unit. -A non-citizen, non-resident, or non-pet unit that is still alive may -even be assigned a tomb zone if they have lost any body part that can -be placed inside a tomb, e.g. teeth or severed limbs. New corpse items -after a tomb has already been assigned will not be properly interred -until the script is executed again on either the unit, its corpse, or -any of its body parts. +A non-citizen, non-resident, or non-pet unit that is still alive +may even be assigned a tomb zone if they have lost any body part +that can be placed inside a tomb, e.g. teeth or severed limbs. +New corpse items after a tomb has already been assigned will not +be properly interred until the script is executed again with the +unit ID specified, or the unit's corpse or any body part selected. If executed on slaughtered animals, all its butchering returns will become valid burial items and no longer usable for cooking or crafting. @@ -34,28 +34,33 @@ become valid burial items and no longer usable for cooking or crafting. Examples -------- -``entomb unit `` +``entomb --unit `` Assign an unassigned tomb zone to the unit with the specified ID. -``entomb tomb `` +``entomb --tomb `` Assign a tomb zone with the specified ID to the selected corpse item's unit. -``entomb unit tomb now`` +``entomb -u -t -h`` Assign a tomb zone with the specified ID to the unit with the - specified ID and teleport its corpse and/or body parts into the - coffin in the tomb zone. + specified ID and task all its burial items for simultaneous + hauling into the coffin in the tomb zone. Options ------- -``unit `` +``-u``, ``--unit `` Specify the ID of the unit to be assigned to a tomb zone. -``tomb `` +``-t``, ``--tomb `` Specify the ID of the zone into which a unit will be interred. -``now`` - Instantly teleport the unit's corpse and/or body parts into the - coffin of its assigned tomb zone. This option can be called on - corpse items or units that are already assigned a tomb zone. +``-a``, ``add-item`` + Add a selected item, or multiple items at the keyboard cursor's + position to be interred together with a unit. A unit or tomb + zone ID must be specified when calling this option. + +``-h``, ``haul-now`` + Task all of the unit's burial items for simultaneous hauling + into the coffin of its assigned tomb zone. This option can be + called even after a tomb zone is already assigned to the unit. diff --git a/entomb.lua b/entomb.lua index fa40d7960c..2160a77fde 100644 --- a/entomb.lua +++ b/entomb.lua @@ -428,7 +428,8 @@ local function ParseCommandLine(args) }, {'a', 'add-item', handler = function() options.addItem = true end}, {'h', 'haul-now', handler = function() options.haulNow = true end}, - {'', 'teleport', handler = function() options.teleport = true end} + -- Commenting out to make this script a non-Armok tool. + -- {'', 'teleport', handler = function() options.teleport = true end} }) return unit, building, options end From 8f4feca6f6fce604fb8d1ea5baaac91ce2225bde Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 30 Jul 2025 03:43:57 +0800 Subject: [PATCH 631/811] Fix argparse short-form conflict --- docs/entomb.rst | 2 +- entomb.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/entomb.rst b/docs/entomb.rst index 64f708a3d0..cef6025199 100644 --- a/docs/entomb.rst +++ b/docs/entomb.rst @@ -60,7 +60,7 @@ Options position to be interred together with a unit. A unit or tomb zone ID must be specified when calling this option. -``-h``, ``haul-now`` +``-n``, ``haul-now`` Task all of the unit's burial items for simultaneous hauling into the coffin of its assigned tomb zone. This option can be called even after a tomb zone is already assigned to the unit. diff --git a/entomb.lua b/entomb.lua index 2160a77fde..dd4f22ca97 100644 --- a/entomb.lua +++ b/entomb.lua @@ -427,7 +427,7 @@ local function ParseCommandLine(args) if not building then qerror('Invalid zone ID.') end end }, {'a', 'add-item', handler = function() options.addItem = true end}, - {'h', 'haul-now', handler = function() options.haulNow = true end}, + {'n', 'haul-now', handler = function() options.haulNow = true end}, -- Commenting out to make this script a non-Armok tool. -- {'', 'teleport', handler = function() options.teleport = true end} }) From 785741bf8043d1148eac8d02ac4b4ca04fe7afd6 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 30 Jul 2025 03:46:38 +0800 Subject: [PATCH 632/811] Fix documentation --- docs/entomb.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/entomb.rst b/docs/entomb.rst index cef6025199..1352b990e4 100644 --- a/docs/entomb.rst +++ b/docs/entomb.rst @@ -55,12 +55,12 @@ Options ``-t``, ``--tomb `` Specify the ID of the zone into which a unit will be interred. -``-a``, ``add-item`` +``-a``, ``--add-item`` Add a selected item, or multiple items at the keyboard cursor's position to be interred together with a unit. A unit or tomb zone ID must be specified when calling this option. -``-n``, ``haul-now`` +``-n``, ``--haul-now`` Task all of the unit's burial items for simultaneous hauling into the coffin of its assigned tomb zone. This option can be called even after a tomb zone is already assigned to the unit. From 8490a13ad4ac16682655b52a6c6acff8758b36f7 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 2 Aug 2025 18:11:38 +0200 Subject: [PATCH 633/811] simplify code and properly split off portions --- changelog.txt | 2 +- immortal-cravings.lua | 117 +++++++++++++++++++++++++++--------------- 2 files changed, 76 insertions(+), 43 deletions(-) diff --git a/changelog.txt b/changelog.txt index 142f3f5452..355043437a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -36,7 +36,7 @@ Template for new versions: - `gui/journal`: fix typo which caused the table of contents to always be regenerated even when not needed - `gui/mod-manager`: gracefully handle mods with missing or broken ``info.txt`` files - `uniform-unstick`: resolve overlap with new buttons in 51.13 -- `immortal-cravings`: prioritize high-value meals and don't go eating or drinking on a full stomach +- `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach ## Misc Improvements diff --git a/immortal-cravings.lua b/immortal-cravings.lua index de251f3fc8..21ae333ecb 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -3,8 +3,18 @@ local idle = reqscript('idle-crafting') local repeatutil = require("repeat-util") + --- utility functions +local verbose = false +---conditional printing of debug messages +---@param message string +local function debug(message) + if verbose then + print(message) + end +end + ---3D city metric ---@param p1 df.coord ---@param p2 df.coord @@ -13,22 +23,20 @@ function distance(p1, p2) return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) + math.abs(p1.z - p2.z) end ----find best item in an item vector (according to some metric) +---maybe a candidate for utils.lua? +---find best available item in an item vector (according to some metric) ---@generic T : df.item ---@param item_vector T[] ----@param metric fun(item: T): number ----@param is_good? fun(item: T): boolean +---@param metric fun(item: T): number? ---@return T? -function findBest(item_vector, metric, is_good) +function findBest(item_vector, metric, smallest) local best = nil - local mbest = -1 - for _,item in ipairs(item_vector) do - if not item.flags.in_job and (not is_good or is_good(item)) then - mitem = metric(item) - if not best or mitem > mbest then - best = item - mbest = mitem - end + local mbest = nil + for _, item in ipairs(item_vector) do + mitem = metric(item) + if mitem and (not best or (smallest and mitem < mbest or mitem > mbest)) then + best = item + mbest = mitem end end return best @@ -41,19 +49,14 @@ end ---@param is_good? fun(item: T): boolean ---@return T? local function findClosest(pos, item_vector, is_good) - local closest = nil - local dclosest = -1 - for _,item in ipairs(item_vector) do - if not item.flags.in_job and (not is_good or is_good(item)) then + local function metric(item) + if not is_good or is_good(item) then local pitem = xyz2pos(dfhack.items.getPosition(item)) - local ditem = distance(pos, pitem) - if dfhack.maps.canWalkBetween(pos, pitem) and (not closest or ditem < dclosest) then - closest = item - dclosest = ditem - end + return dfhack.maps.canWalkBetween(pos, pitem) and distance(pos, pitem) or nil end + return nil end - return closest + return findBest(item_vector, metric, true) end ---find a drink @@ -62,33 +65,32 @@ end local function get_closest_drink(pos) local is_good = function (drink) local container = dfhack.items.getContainer(drink) - return container and container:isFoodStorage() + return not drink.flags.in_job and container and container:isFoodStorage() end return findClosest(pos, df.global.world.items.other.DRINK, is_good) end ----find highest-value accessible meal +---find available meal with highest per-portion value ---@return df.item_foodst? local function get_best_meal(pos) ---@param meal df.item_foodst - local function is_good(meal) + local function portion_value(meal) local accessible = dfhack.maps.canWalkBetween(pos,xyz2pos(dfhack.items.getPosition(meal))) - if meal.flags.rotten or not accessible then - return false + if meal.flags.in_job or meal.flags.rotten or not accessible then + return nil else -- check that meal is either on the ground or in food storage (and not in a backpack) local container = dfhack.items.getContainer(meal) - return not container or container:isFoodStorage() + if not container or container:isFoodStorage() then + return dfhack.items.getValue(meal) / meal.stack_size + else + return nil + end end end - ---@param meal df.item_foodst - local function portion_value(meal) - return dfhack.items.getValue(meal) / meal.stack_size - end - - return findBest(df.global.world.items.other.FOOD, portion_value, is_good) + return findBest(df.global.world.items.other.FOOD, portion_value) end ---create a Drink job for the given unit @@ -116,11 +118,22 @@ end ---create Eat job for the given unit ---@param unit df.unit local function goEat(unit) - local meal = get_best_meal(unit.pos) - if not meal then + local meal_stack = get_best_meal(unit.pos) + if not meal_stack then -- print('no accessible meals found') return end + + ---@type df.item|df.item_foodst + local meal + if meal_stack.stack_size > 1 then + meal = meal_stack:splitStack(1, true) + meal:categorize(true) + else + meal = meal_stack + end + dfhack.items.setOwner(meal, unit) + local job = idle.make_job() job.job_type = df.job_type.Eat job.flags.special = true @@ -135,6 +148,25 @@ local function goEat(unit) print(dfhack.df2console('immortal-cravings: %s is getting something to eat'):format(name)) end +---unit is ready to take jobs (will interrupt social activities) +---@param unit df.unit +---@return boolean +function unitIsAvailable(unit) + if unit.job.current_job then + return false + elseif #unit.individual_drills > 0 then + return false + elseif unit.flags1.caged or unit.flags1.chained then + return false + elseif unit.military.squad_id ~= -1 then + local squad = df.squad.find(unit.military.squad_id) + -- this lookup should never fail + ---@diagnostic disable-next-line: need-check-nil + return #squad.orders == 0 and squad.activity == -1 + end + return true +end + --- script logic local GLOBAL_KEY = 'immortal-cravings' @@ -167,7 +199,7 @@ local threshold = -9000 ---unit loop: check for idle watched units and create eat/drink jobs for them local function unit_loop() - -- print(('immortal-cravings: running unit loop (%d watched units)'):format(#watched)) + debug(('immortal-cravings: running unit loop (%d watched units)'):format(#watched)) ---@type integer[] local kept = {} for _, unit_id in ipairs(watched) do @@ -178,7 +210,8 @@ local function unit_loop() then goto next_unit end - if not idle.unitIsAvailable(unit) then + if not unitIsAvailable(unit) then + debug("immortal-cravings: skipping busy"..dfhack.units.getReadableName(unit)) table.insert(kept, unit.id) else -- unit is available for jobs; satisfy one of its needs @@ -196,7 +229,7 @@ local function unit_loop() end watched = kept if #watched == 0 then - -- print('immortal-cravings: no more watched units, cancelling unit loop') + debug('immortal-cravings: no more watched units, cancelling unit loop') repeatutil.cancel(GLOBAL_KEY .. '-unit') end end @@ -208,9 +241,9 @@ end ---main loop: look for citizens with personality needs for food/drink but w/o physiological need local function main_loop() - -- print('immortal-cravings watching:') + debug('immortal-cravings watching:') watched = {} - for _, unit in ipairs(dfhack.units.getCitizens()) do + for _, unit in ipairs(dfhack.units.getCitizens(false, false)) do if not (is_active_caste_flag(unit, 'NO_DRINK') or is_active_caste_flag(unit, 'NO_EAT')) or unit.counters2.stomach_content > 0 @@ -222,7 +255,7 @@ local function main_loop() need.id == EatGoodMeal and need.focus_level < threshold then table.insert(watched, unit.id) - -- print(' '..dfhack.df2console(dfhack.units.getReadableName(unit))) + debug(' '..dfhack.df2console(dfhack.units.getReadableName(unit))) goto next_unit end end From ab7dc7654b5c53aff0bc8821b69531544e4f1d76 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Wed, 6 Aug 2025 18:18:22 -0500 Subject: [PATCH 634/811] prevent `make-legendary` from assigning skill -1 fixes DFHack/dfhack#5541 --- changelog.txt | 1 + make-legendary.lua | 12 ++++++++---- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/changelog.txt b/changelog.txt index 8b79bd7b2d..c6324adf87 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `make-legendary`: ``make-legendary all`` will no longer corrupt souls ## Misc Improvements diff --git a/make-legendary.lua b/make-legendary.lua index 2098ad0ba2..a0c96c0cb2 100644 --- a/make-legendary.lua +++ b/make-legendary.lua @@ -7,9 +7,11 @@ function getName(unit) end function legendize(unit, skill_idx) - utils.insert_or_update(unit.status.current_soul.skills, - {new=true, id=skill_idx, rating=df.skill_rating.Legendary5}, - 'id') + if skill_idx >= 0 and skill_idx <= df.job_skill._last_item then + utils.insert_or_update(unit.status.current_soul.skills, + {new=true, id=skill_idx, rating=df.skill_rating.Legendary5}, + 'id') + end end function make_legendary(skillname) @@ -50,7 +52,9 @@ function BreathOfArmok() return end for i in ipairs(df.job_skill) do - legendize(unit, i) + if i >= 0 then + legendize(unit, i) + end end print('The breath of Armok has engulfed ' .. getName(unit)) end From 61e0e181953e70679d0ed4dfc7e9e64922ad0c07 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Thu, 7 Aug 2025 12:53:25 -0500 Subject: [PATCH 635/811] Update changelog for 52.03-r1 --- changelog.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/changelog.txt b/changelog.txt index c6324adf87..23e458faf1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,18 @@ Template for new versions: ## New Features +## Fixes + +## Misc Improvements + +## Removed + +# 52.03-r1 + +## New Tools + +## New Features + ## Fixes - `make-legendary`: ``make-legendary all`` will no longer corrupt souls From 062ef18de351faf25a471ab1d8e562d9515b3cb7 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 13 Aug 2025 11:42:50 +0800 Subject: [PATCH 636/811] Assign only active tomb zones and make it unavailable for auto assigment to other units --- entomb.lua | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/entomb.lua b/entomb.lua index dd4f22ca97..735b2d65f3 100644 --- a/entomb.lua +++ b/entomb.lua @@ -52,7 +52,14 @@ end -- Iterate through all available tomb zones. local function IterateTombZones(unit_id) for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do - if CheckTombZone(building, unit_id) then return building end + if unit_id == -1 then + -- Use only active (unpaused) zones when assigning unassigned tomb zones. + if building.spec_sub_flag.active then + if CheckTombZone(building, unit_id) then return building end + end + else + if CheckTombZone(building, unit_id) then return building end + end end return nil end @@ -60,9 +67,9 @@ end -- Use when user inputs coffin building ID instead of tomb zone ID. function GetTombFromCoffin(building) if #building.relations > 0 then - for _, v in ipairs(building.relations) do - if df.building_civzonest:is_instance(v) and v.type == df.civzone_type.Tomb then - return v + for _, zone in ipairs(building.relations) do + if df.building_civzonest:is_instance(zone) and zone.type == df.civzone_type.Tomb then + return zone end end end @@ -134,6 +141,7 @@ function AssignToTomb(unit, tomb) local incident = df.incident.find(incident_id) -- Corpse will not be interred if not yet discovered, -- which never happens for units not belonging to player's civ. + -- Only needed for units that have a death incident. incident.flags.discovered = true end local burialItemCount = FlagForBurial(unit, corpseParts) @@ -145,6 +153,9 @@ function AssignToTomb(unit, tomb) if not utils.linear_index(unit.owned_buildings, tomb) then unit.owned_buildings:insert('#', tomb) end + -- Make tomb zone unavailable for automatic assignment to other dead units. + tomb.zone_settings.tomb.flags.no_pets = true + tomb.zone_settings.tomb.flags.no_citizens = true print(string.format(strBurial, strUnitName, strTomb)) print(string.format(strCorpseItems, burialItemCount, strPlural, strPlural)) end From cdf3ebb2512e19c94a7c3a760b5ffd7f3cf84140 Mon Sep 17 00:00:00 2001 From: Jarkami Date: Wed, 13 Aug 2025 00:36:46 -0400 Subject: [PATCH 637/811] Fix uniform assignment state inconsistency caused by uniform-unstick --- changelog.txt | 1 + uniform-unstick.lua | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/changelog.txt b/changelog.txt index 23e458faf1..5f1a881f48 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,6 +31,7 @@ Template for new versions: ## New Features ## Fixes +- `uniform-unstick`: no longer causes units to equip multiples of assigned items ## Misc Improvements diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 0fb501fd2d..929ca71d7b 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -196,6 +196,22 @@ local function process(unit, args, need_newline) end end + -- Make the equipment.assigned_items list consistent with what is present in equipment.uniform + for i=#(squad_position.equipment.assigned_items)-1,0,-1 do + local u_id = squad_position.equipment.assigned_items[i] + -- Quiver, backpack, and flask are assigned in their own locations rather than in equipment.uniform, and thus need their own checks + -- If more separately-assigned items are added in the future, this handling will need to be updated accordingly + if assigned_items[u_id] == nil and u_id ~= squad_position.equipment.quiver and u_id ~= squad_position.equipment.backpack and u_id ~= squad_position.equipment.flask then + local item = df.item.find(u_id) + if item ~= nil then + need_newline = print_line(unit_name .. " has an improperly assigned item, item # " .. u_id .. " '" .. item_description(item) .. "'; removing it") + else + need_newline = print_line(unit_name .. " has a nonexistent item assigned, item # " .. u_id .. "; removing it") + end + squad_position.equipment.assigned_items:erase(i) + end + end + -- Figure out which worn items should be dropped -- First, figure out which body parts are covered by the uniform pieces we have. From fb3f2d1b32b09605823677df00992b219dddce98 Mon Sep 17 00:00:00 2001 From: Jarkami Date: Wed, 13 Aug 2025 00:41:00 -0400 Subject: [PATCH 638/811] Refactor item description logging in uniform-unstick --- uniform-unstick.lua | 21 +++++++++------------ 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 929ca71d7b..7ea2b33d86 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -16,7 +16,7 @@ local validArgs = utils.invert({ -- Functions local function item_description(item) - return dfhack.df2console(dfhack.items.getDescription(item, 0, true)) + return "item #" .. item.id .. " '" .. dfhack.df2console(dfhack.items.getDescription(item, 0, true)) .. "'" end local function get_item_pos(item) @@ -166,11 +166,10 @@ local function process(unit, args, need_newline) for u_id, item in pairs(assigned_items) do if not worn_items[u_id] then if not silent then - need_newline = print_line(unit_name .. " is missing an assigned item, object #" .. u_id .. " '" .. - item_description(item) .. "'", need_newline) + need_newline = print_line(unit_name .. " is missing an assigned item, " .. item_description(item), need_newline) end if dfhack.items.getGeneralRef(item, df.general_ref_type.UNIT_HOLDER) then - need_newline = print_line(unit_name .. " cannot equip item: another unit has a claim on object #" .. u_id .. " '" .. item_description(item) .. "'", need_newline) + need_newline = print_line(unit_name .. " cannot equip item: another unit has a claim on " .. item_description(item), need_newline) if args.free then print(" Removing from uniform") assigned_items[u_id] = nil @@ -204,9 +203,9 @@ local function process(unit, args, need_newline) if assigned_items[u_id] == nil and u_id ~= squad_position.equipment.quiver and u_id ~= squad_position.equipment.backpack and u_id ~= squad_position.equipment.flask then local item = df.item.find(u_id) if item ~= nil then - need_newline = print_line(unit_name .. " has an improperly assigned item, item # " .. u_id .. " '" .. item_description(item) .. "'; removing it") + need_newline = print_line(unit_name .. " has an improperly assigned item, " .. item_description(item) .. '; removing it') else - need_newline = print_line(unit_name .. " has a nonexistent item assigned, item # " .. u_id .. "; removing it") + need_newline = print_line(unit_name .. " has a nonexistent item assigned, item # " .. u_id .. '; removing it') end squad_position.equipment.assigned_items:erase(i) end @@ -240,9 +239,7 @@ local function process(unit, args, need_newline) for w_id, item in pairs(worn_items) do if assigned_items[w_id] == nil then -- don't drop uniform pieces (including shields, weapons for hands) if uncovered[worn_parts[w_id]] then - need_newline = print_line(unit_name .. - " potentially has object #" .. - w_id .. " '" .. item_description(item) .. "' blocking a missing uniform item.", need_newline) + need_newline = print_line(unit_name .. " potentially has " .. item_description(item) .. " blocking a missing uniform item.", need_newline) if args.drop then to_drop[w_id] = item end @@ -261,12 +258,12 @@ local function do_drop(item_list) for id, item in pairs(item_list) do local pos = get_item_pos(item) if not pos then - dfhack.printerr("Could not find drop location for item #" .. id .. " " .. item_description(item)) + dfhack.printerr("Could not find drop location for " .. item_description(item)) else if dfhack.items.moveToGround(item, pos) then - print("Dropped item #" .. id .. " '" .. item_description(item) .. "'") + print("Dropped " .. item_description(item)) else - dfhack.printerr("Could not drop object #" .. id .. " " .. item_description(item)) + dfhack.printerr("Could not drop " .. item_description(item)) end end end From 5f1c7b7658dc6f7c1902077e5ac416f13e711477 Mon Sep 17 00:00:00 2001 From: git--amade Date: Wed, 13 Aug 2025 13:32:38 +0800 Subject: [PATCH 639/811] Remove duplicated code in IterateTombZones() --- entomb.lua | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/entomb.lua b/entomb.lua index 735b2d65f3..0455f6b71e 100644 --- a/entomb.lua +++ b/entomb.lua @@ -52,14 +52,10 @@ end -- Iterate through all available tomb zones. local function IterateTombZones(unit_id) for _, building in ipairs(df.global.world.buildings.other.ZONE_TOMB) do - if unit_id == -1 then - -- Use only active (unpaused) zones when assigning unassigned tomb zones. - if building.spec_sub_flag.active then - if CheckTombZone(building, unit_id) then return building end - end - else - if CheckTombZone(building, unit_id) then return building end - end + -- Use only active (unpaused) zones when assigning unassigned tomb zones. + if unit_id == -1 and not building.spec_sub_flag.active then goto skipIteration end + if CheckTombZone(building, unit_id) then return building end + ::skipIteration:: end return nil end From fb76d7b3b46fe07917dd7cb549fe711ca3de386b Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Sat, 16 Aug 2025 19:49:07 +0200 Subject: [PATCH 640/811] new tool: husbandry --- changelog.txt | 2 + docs/husbandry.rst | 61 +++++++++ husbandry.lua | 326 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 389 insertions(+) create mode 100644 docs/husbandry.rst create mode 100644 husbandry.lua diff --git a/changelog.txt b/changelog.txt index 23e458faf1..9ea3bc988c 100644 --- a/changelog.txt +++ b/changelog.txt @@ -28,6 +28,8 @@ Template for new versions: ## New Tools +- `husbandry`: Automatically milk and shear animals at nearby farmer's workshops + ## New Features ## Fixes diff --git a/docs/husbandry.rst b/docs/husbandry.rst new file mode 100644 index 0000000000..27fa459cfe --- /dev/null +++ b/docs/husbandry.rst @@ -0,0 +1,61 @@ +husbandry +========= + +.. dfhack-tool:: + :summary: Automatically milk and shear animals. + :tags: fort auto + +This tool will automatically create milking and shearing orders at farmer's +workshops. Unlike the ``automilk`` and ``autoshear`` options from the control +panel, which create general work orders for milking and shearing jobs, +``husbandry`` will directly create jobs for individual animals at specific +workshops. This allows milking and shearing jobs to reliably be created at +nearby workshops (e.g. inside the pasture that an animal is assigned to), +minimizing the labor required to re-pasture animals after milking or shearing, +in particular in the case of multiple pastures that are far apart. + + +Usage +----- + +:: + + enable husbandry + husbandry [status] + husbandry now + husbandry [set|unset] [shearing|milking|roaming|pasture]+ + +Flags can be set or unset using the command ``husbandry set`` or ``husbandry +unset``. The ``shearing`` and ``milking`` flags (both enabled by default) +control whether shearing or milking jobs are created at all. + +Further, ``husbandry`` distinguishes between animals that are assigned to +pastures and those that are "roaming". + +If an animal is pastured and the pasture contains at least one workshop with the +appropriate labour (i.e. milking or shearing) enabled, jobs will be created +exclusively at those workshops. If the pasture does not contain a workshop with +the appropriate labor enabled the behavior depends on the ``pasture`` flag +(disabled by default): if set, no jobs will be created at workshops outside of +pastures, otherwise jobs may be created at the closest workshop in your fort. + +For animals that are roaming, jobs will only be created if the ``roaming`` flag +is set, which is the default. In this case, jobs are created at the closest +workshop with the appropriate labours enabled. + +Examples +-------- + +``enable husbandry`` + Start generating milking and shearing orders for animals. + +``husbandry now`` + Run a single cycle, detecting animals that can be milked/sheared an creating + jobs. Does not require the tool to be enabled. + +``husbandry unset roaming`` + Disable the creation of jobs for roaming animals. + +``husbandry set milking shearing pasture`` + Create milking and shearing jobs for pastured animals, but only at workshops + inside their pastures. diff --git a/husbandry.lua b/husbandry.lua new file mode 100644 index 0000000000..f8dd1375ec --- /dev/null +++ b/husbandry.lua @@ -0,0 +1,326 @@ + +--@enable = true +--@module = true + +local utils = require 'utils' +local repeatutil = require("repeat-util") +local ic = reqscript('idle-crafting') + +local verbose = true +---conditional printing of debug messages +---@param message string +local function debug(message) + if verbose then + print(message) + end +end + +-- From workorder.lua +---------------------------8<----------------------------- + +local function isValidAnimal(unit) + -- this should also check for the absence of misc trait 55 (as of 50.09), but we don't + -- currently have an enum definition for that value yet + return dfhack.units.isOwnCiv(unit) + and dfhack.units.isAlive(unit) + and dfhack.units.isAdult(unit) + and dfhack.units.isActive(unit) + and dfhack.units.isFortControlled(unit) + and dfhack.units.isTame(unit) + and not dfhack.units.isMarkedForSlaughter(unit) + and not dfhack.units.getMiscTrait(unit, df.misc_trait_type.Migrant, false) +end + +-- true/false or nil if no shearable_tissue_layer with length > 0. +local function canShearCreature(unit) + local stls = df.global.world.raws.creatures + .all[unit.race] + .caste[unit.caste] + .shearable_tissue_layer + + local any + for _, stl in ipairs(stls) do + if stl.length > 0 then + for _, bpi in ipairs(stl.bp_modifiers_idx) do + any = { unit.appearance.bp_modifiers[bpi], stl.length } + if unit.appearance.bp_modifiers[bpi] >= stl.length then + return true, any + end + end + end + end + + if any then return false, any end + -- otherwise: nil +end + +---------------------------8<----------------------------- + +local function canMilkCreature(u) + if dfhack.units.isMilkable(u) and not dfhack.units.isPet(u) then + local mt_milk = dfhack.units.getMiscTrait(u, df.misc_trait_type.MilkCounter, false) + if not mt_milk then return true else return false end + else + return nil + end +end + +---@param p1 df.coord +---@param p2 df.coord +---@return number +function distance(p1, p2) + return math.max(math.abs(p1.x - p2.x), math.abs(p1.y - p2.y)) + 2 * math.abs(p1.z - p2.z) +end + +---find appropriate workshop to milk or shear an animal +---@param unit df.unit +---@param collection table +---@return df.building_workshopst? +local function getAppropriateWorkshop(unit, collection) + local zone_ref = dfhack.units.getGeneralRef(unit, df.general_ref_type.BUILDING_CIVZONE_ASSIGNED) + local zone = zone_ref and zone_ref:getBuilding() or nil + + -- if animal is assigned to a zone containing workshops, only use those + if zone then + local contains_workshop = false + local best = nil + local worst_load = 10 + for _, workshop in pairs(collection[zone.z] or {}) do + if dfhack.buildings.containsTile(zone, workshop.centerx, workshop.centery) then + contains_workshop = true + local workshop_pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z) + if dfhack.maps.canWalkBetween(unit.pos, workshop_pos) and #workshop.jobs < worst_load then + worst_load = #workshop.jobs + best = workshop + end + end + end + if contains_workshop or state.pasture then + return best + end + elseif not state.roaming then + return nil -- not treating roaming animals + end + -- otherwise, use the closest workshop to the animal + local closest = nil + local dist = nil + for _, level in pairs(collection) do + for _, workshop in pairs(level) do + local workshop_pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z) + if dfhack.maps.canWalkBetween(unit.pos, workshop_pos) then + local d = distance(unit.pos, workshop_pos) + if not closest or d < dist then + closest = workshop + dist = d + end + end + end + end + return #closest.jobs < 10 and closest or nil +end + +local function shearCreature(unit, workshop) + local job = ic.make_job() + job.job_type = df.job_type.ShearCreature + dfhack.job.addGeneralRef(job, df.general_ref_type.UNIT_SHEAREE, unit.id) + ic.assignToWorkshop(job, workshop) +end + +local function milkCreature(unit, workshop) + local job = ic.make_job() + job.job_type = df.job_type.MilkCreature + dfhack.job.addGeneralRef(job, df.general_ref_type.UNIT_MILKEE, unit.id) + ic.assignToWorkshop(job, workshop) +end + + +-- configuration management + +GLOBAL_KEY = 'husbandry' + +local function get_default_state() + return { + enabled = false, + milking = true, + shearing = true, + roaming = true; + pasture = false + } +end + +state = state or get_default_state() + +function isEnabled() + return state.enabled +end + +function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=state.enabled, + milking=state.milking, + shearing=state.shearing, + roaming=state.roaming, + pasture=state.pasture, + }) +end + +--- Load the saved state of the script +local function load_state() + -- load persistent data + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, get_default_state()) + state.enabled = persisted_data.enabled + state.milking = persisted_data.milking + state.shearing = persisted_data.shearing + state.roaming = persisted_data.roaming + state.pasture = persisted_data.pasture + return state +end + +-- main script action + +local function action() + debug('husbandry: running loop') + + -- organize workshops by allowed labors and z-level + ---@type table + local farmer_shearing = {} + ---@type table + local farmer_milking = {} + for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do + if not workshop.profile.blocked_labors[df.unit_labor.SHEARER] then + table.insert(ensure_key(farmer_shearing, workshop.z), workshop) + end + if not workshop.profile.blocked_labors[df.unit_labor.MILK] then + table.insert(ensure_key(farmer_milking, workshop.z), workshop) + end + end + + -- gather units that are already being milked or sheared + ---@type table + local unit_milking = {} + ---@type table + local unit_shearing = {} + + -- go over all workshops to to catch player-initiated jobs + for _, workshop in ipairs(df.global.world.buildings.other.WORKSHOP_FARMER) do + for _, job in ipairs(workshop.jobs) do + if state.milking and job.job_type == df.job_type.MilkCreature then + local milkee = dfhack.job.getGeneralRef(job, df.general_ref_type.UNIT_MILKEE) + if milkee then + unit_milking[milkee.unit_id] = true + end + elseif state.shearing and job.job_type == df.job_type.ShearCreature then + local shearee = dfhack.job.getGeneralRef(job, df.general_ref_type.UNIT_SHEAREE) + if shearee then + unit_shearing[shearee.unit_id] = true + end + end + end + end + + -- look for units that can be milked/sheared and for which there is no active job + for _, unit in ipairs(df.global.world.units.active) do + if not isValidAnimal(unit) then goto skip end + + if state.shearing and canShearCreature(unit) and not unit_shearing[unit.id] then + local workshop = getAppropriateWorkshop(unit, farmer_shearing) + if workshop then + shearCreature(unit, workshop) + end + end + + if state.milking and canMilkCreature(unit) and not unit_milking[unit.id] then + local workshop = getAppropriateWorkshop(unit, farmer_milking) + if workshop then + milkCreature(unit, workshop) + end + end + + ::skip:: + end +end + +-- enable management + +local function start() + if state.enabled then + repeatutil.scheduleUnlessAlreadyScheduled(GLOBAL_KEY, 1000, 'ticks', action) + end +end + +local function stop() + repeatutil.cancel(GLOBAL_KEY) +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + state.enabled = false + return + end + + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + return + end + + load_state() + start() +end + +if dfhack_flags.module then + return +end + +if dfhack_flags.enable then + if dfhack_flags.enable_state then + enabled = true + start() + else + enabled = false + stop() + end + persist_state() + return +end + +-- command-line interface + +local argparse = require('argparse') +local positionals = argparse.processArgsGetopt({ ... }, {}) + +local state_vars = utils.invert({ "milking", "shearing", "roaming", "pasture" }) + +local function setFlags(positionals, value) + for i = 2, #positionals do + local flag = positionals[i] + if state_vars[flag] then + debug(("setting %s = %s"):format(flag, value)) + state[flag] = value + end + end +end + +load_state() +if not positionals[1] or positionals[1] == 'status' then + print(("husbandry is %s"):format(state.enabled and "enabled" or "not enabled")) + print(("currently %smilking%s%sshearing animals"):format( + state.milking and "" or "not ", + state.milking == state.shearing and " and " or " but ", + state.shearing and "" or "not ")) + print(("%s roaming animals"):format(state.roaming and "including" or "ignoring")) + if state.pasture then + print("not milking/shearing animals inside pastures without workshops") + end +elseif positionals[1] == "set" then + if positionals[2] == "default" then + state = get_default_state() + else + setFlags(positionals, true) + end +elseif positionals[1] == "unset" then + setFlags(positionals, false) +elseif positionals[1] == "now" then + action() +else + qerror("unrecognized option") +end +persist_state() From 0831e6e0ebd559c1debd2ca5f916bca781874f30 Mon Sep 17 00:00:00 2001 From: Jarkami Date: Sun, 17 Aug 2025 02:20:49 -0400 Subject: [PATCH 641/811] Clean up/improve code readability in uniform-unstick --- uniform-unstick.lua | 162 +++++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 68 deletions(-) diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 7ea2b33d86..bd8b550ba9 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -15,11 +15,15 @@ local validArgs = utils.invert({ -- Functions +-- @param item df.item +-- @return string local function item_description(item) return "item #" .. item.id .. " '" .. dfhack.df2console(dfhack.items.getDescription(item, 0, true)) .. "'" end -local function get_item_pos(item) +-- @param item df.item +-- @return df.coord|nil +local function get_visible_item_pos(item) local x, y, z = dfhack.items.getPosition(item) if not x or not y or not z then return @@ -30,24 +34,30 @@ local function get_item_pos(item) end end -local function get_squad_position(unit, unit_name) +-- @param unit df.unit +-- @return df.squad_position|nil +local function get_squad_position(unit) local squad = df.squad.find(unit.military.squad_id) - if squad then - if squad.entity_id ~= df.global.plotinfo.group_id then - print("WARNING: Unit " .. unit_name .. " is a member of a squad from another site!" .. - " This may be preventing them from doing any useful work." .. - " You can fix this by assigning them to a local squad and then unassigning them.") - print() - return - end - else + if not squad then + return + end + + if squad.entity_id ~= df.global.plotinfo.group_id then + print("WARNING: Unit " .. dfhack.df2console(dfhack.units.getReadableName(unit)) .. " is a member of a squad from another site!" .. + " This may be preventing them from doing any useful work." .. + " You can fix this by assigning them to a local squad and then unassigning them.") + print() return end + if #squad.positions > unit.military.squad_position then return squad.positions[unit.military.squad_position] end end +-- @param unit df.unit +-- @param item df.item +-- @return number[] list of body part ids local function bodyparts_that_can_wear(unit, item) local bodyparts = {} local unitparts = dfhack.units.getCasteRaw(unit).body_info.body_parts @@ -89,47 +99,61 @@ local function bodyparts_that_can_wear(unit, item) return bodyparts end --- returns new value of need_newline -local function print_line(text, need_newline) - if need_newline then - print() - end - print(text) - return false +-- @param unit_name string +-- @param labor_name string +local function print_bad_labor(unit_name, labor_name) + return print("WARNING: Unit " .. unit_name .. " has the " .. labor_name .. + " labor enabled, which conflicts with military uniforms.") end -local function print_bad_labor(unit_name, labor_name, need_newline) - return print_line("WARNING: Unit " .. unit_name .. " has the " .. labor_name .. - " labor enabled, which conflicts with military uniforms.", need_newline) +-- @param squad_position df.squad_position +-- @param item_id number +local function remove_item_from_position(squad_position, item_id) + for _, uniform_slot_specs in ipairs(squad_position.equipment.uniform) do + for _, uniform_spec in ipairs(uniform_slot_specs) do + for idx, assigned_item_id in ipairs(uniform_spec.assigned) do + if assigned_item_id == item_id then + uniform_spec.assigned:erase(idx) + return + end + end + end + end end -- Will figure out which items need to be moved to the floor, returns an item_id:item map -local function process(unit, args, need_newline) +local function process(unit, args) local silent = args.all -- Don't print details if we're iterating through all dwarves local unit_name = dfhack.df2console(dfhack.units.getReadableName(unit)) + local printed = false if not silent then - need_newline = print_line("Processing unit " .. unit_name, need_newline) + print("Processing unit " .. unit_name) + printed = true end -- The return value local to_drop = {} -- item id to item object -- First get squad position for an early-out for non-military dwarves - local squad_position = get_squad_position(unit, unit_name) + local squad_position = get_squad_position(unit) if not squad_position then if not silent then - need_newline = print_line(unit_name .. " does not have a military uniform.", need_newline) + print(unit_name .. " does not have a military uniform.") + print() end return end if unit.status.labors.MINE then - need_newline = print_bad_labor(unit_name, "mining", need_newline) + print_bad_labor(unit_name, "mining") + printed = true elseif unit.status.labors.CUTWOOD then - need_newline = print_bad_labor(unit_name, "woodcutting", need_newline) + print_bad_labor(unit_name, "woodcutting") + printed = true elseif unit.status.labors.HUNT then - need_newline = print_bad_labor(unit_name, "hunting", need_newline) + print_bad_labor(unit_name, "hunting") + printed = true end -- Find all worn items which may be at issue. @@ -148,12 +172,12 @@ local function process(unit, args, need_newline) end -- Now get info about which items have been assigned as part of the uniform - local assigned_items = {} -- assigned item ids mapped to item objects - for _, specs in ipairs(squad_position.equipment.uniform) do - for _, spec in ipairs(specs) do - for _, assigned in ipairs(spec.assigned) do + local uniform_assigned_items = {} -- assigned item ids mapped to item objects + for _, uniform_slot_specs in ipairs(squad_position.equipment.uniform) do + for _, uniform_spec in ipairs(uniform_slot_specs) do + for _, assigned_item_id in ipairs(uniform_spec.assigned) do -- Include weapon and shield so we can avoid dropping them, or pull them out of container/inventory later - assigned_items[assigned] = df.item.find(assigned) + uniform_assigned_items[assigned_item_id] = df.item.find(assigned_item_id) end end end @@ -163,50 +187,48 @@ local function process(unit, args, need_newline) local present_ids = {} -- map of item ID to item object local missing_ids = {} -- map of item ID to item object - for u_id, item in pairs(assigned_items) do - if not worn_items[u_id] then + for item_id, item in pairs(uniform_assigned_items) do + if not worn_items[item_id] then if not silent then - need_newline = print_line(unit_name .. " is missing an assigned item, " .. item_description(item), need_newline) + print(unit_name .. " is missing an assigned item, " .. item_description(item)) + printed = true end if dfhack.items.getGeneralRef(item, df.general_ref_type.UNIT_HOLDER) then - need_newline = print_line(unit_name .. " cannot equip item: another unit has a claim on " .. item_description(item), need_newline) + print(unit_name .. " cannot equip item: another unit has a claim on " .. item_description(item)) + printed = true if args.free then print(" Removing from uniform") - assigned_items[u_id] = nil - for _, specs in ipairs(squad_position.equipment.uniform) do - for _, spec in ipairs(specs) do - for idx, assigned in ipairs(spec.assigned) do - if assigned == u_id then - spec.assigned:erase(idx) - break - end - end - end - end + uniform_assigned_items[item_id] = nil + remove_item_from_position(squad_position, item_id) end else - missing_ids[u_id] = item + missing_ids[item_id] = item if args.free then - to_drop[u_id] = item + to_drop[item_id] = item end end else - present_ids[u_id] = item + present_ids[item_id] = item end end -- Make the equipment.assigned_items list consistent with what is present in equipment.uniform for i=#(squad_position.equipment.assigned_items)-1,0,-1 do - local u_id = squad_position.equipment.assigned_items[i] + local assigned_item_id = squad_position.equipment.assigned_items[i] -- Quiver, backpack, and flask are assigned in their own locations rather than in equipment.uniform, and thus need their own checks -- If more separately-assigned items are added in the future, this handling will need to be updated accordingly - if assigned_items[u_id] == nil and u_id ~= squad_position.equipment.quiver and u_id ~= squad_position.equipment.backpack and u_id ~= squad_position.equipment.flask then - local item = df.item.find(u_id) + if uniform_assigned_items[assigned_item_id] == nil and + assigned_item_id ~= squad_position.equipment.quiver and + assigned_item_id ~= squad_position.equipment.backpack and + assigned_item_id ~= squad_position.equipment.flask + then + local item = df.item.find(assigned_item_id) if item ~= nil then - need_newline = print_line(unit_name .. " has an improperly assigned item, " .. item_description(item) .. '; removing it') + print(unit_name .. " has an improperly assigned item, " .. item_description(item) .. "; removing it") else - need_newline = print_line(unit_name .. " has a nonexistent item assigned, item # " .. u_id .. '; removing it') + print(unit_name .. " has a nonexistent item assigned, item # " .. assigned_item_id .. "; removing it") end + printed = true squad_position.equipment.assigned_items:erase(i) end end @@ -217,10 +239,10 @@ local function process(unit, args, need_newline) -- unless --multi is specified, in which we don't care local covered = {} -- map of body part id to true/nil if not args.multi then - for id, item in pairs(present_ids) do + for item_id, item in pairs(present_ids) do -- weapons and shields don't "cover" the bodypart they're assigned to. (Needed to figure out if we're missing gloves.) if item._type ~= df.item_weaponst and item._type ~= df.item_shieldst then - covered[worn_parts[id]] = true + covered[worn_parts[item_id]] = true end end end @@ -236,17 +258,23 @@ local function process(unit, args, need_newline) end -- Drop everything (except uniform pieces) from body parts which should be covered but aren't - for w_id, item in pairs(worn_items) do - if assigned_items[w_id] == nil then -- don't drop uniform pieces (including shields, weapons for hands) - if uncovered[worn_parts[w_id]] then - need_newline = print_line(unit_name .. " potentially has " .. item_description(item) .. " blocking a missing uniform item.", need_newline) + for worn_item_id, item in pairs(worn_items) do + if uniform_assigned_items[worn_item_id] == nil then -- don't drop uniform pieces (including shields, weapons for hands) + if uncovered[worn_parts[worn_item_id]] then + print(unit_name .. " potentially has " .. item_description(item) .. " blocking a missing uniform item.") + printed = true if args.drop then - to_drop[w_id] = item + to_drop[worn_item_id] = item end end end end + -- add a spacing line if there was any output + if printed then + print() + end + return to_drop end @@ -255,8 +283,8 @@ local function do_drop(item_list) return end - for id, item in pairs(item_list) do - local pos = get_item_pos(item) + for _, item in pairs(item_list) do + local pos = get_visible_item_pos(item) if not pos then dfhack.printerr("Could not find drop location for " .. item_description(item)) else @@ -278,10 +306,8 @@ local function main(args) end if args.all then - local need_newline = false for _, unit in ipairs(dfhack.units.getCitizens(true)) do - do_drop(process(unit, args, need_newline)) - need_newline = true + do_drop(process(unit, args)) end else local unit = dfhack.gui.getSelectedUnit() From a02935d0bdf833486e80cf39351ea9edd2e48727 Mon Sep 17 00:00:00 2001 From: Squid Coder <92821989+realSquidCoder@users.noreply.github.com> Date: Sun, 17 Aug 2025 12:31:14 -0500 Subject: [PATCH 642/811] New Feature: `autotraining` (#1411) * New Feature: `gym` Code for dwarves to hit the gym when they yearn for the gains. Assigns Dwarves to a military squad until they have fulfilled their need for Martial Training * Fix whitespace * missed some * MORE whitespace (and some other cleanup) * Update gym.lua * Create gym.rst * Fix EOF * Update gym.rst * fix key error * more key errors * Update the documentation * Use the enable/disable stuff not args to start or stop * Do the documentation in one place * Various fixes - Clean up documentation - Add option to change squad name. - persist the enabled state, the threshold, and the squad name. - fixed findNeed function - renamed script to `autotraining` - made the ignore flag more clear and more changable - fixed 1 sided military link in `addTraining` * More cleanup Also tell the user when data was persisted (mostly for debugging) * rename the script itself and update the docs to account. * fix docs * Add credit where credit is due * add to control panel alert the user if the squad cant be found (since we cant reliably make a squad ourselves... yet) * Check the squad's entity_id to make sure we get *our* Gym * Update autotraining.lua remove the `.` because it could lead to confusion * Fix the ignore count never being reset * Fix units that need training but are already doing so being reported as queued * fix the ignore count (it should be global) * Apply suggestions from code review * fix typo * fix to actually check the unit's squad * Update for gui usage * clean up * initial gui and update from code review * show alias in gui too * clean up * Create gui docs * update the docs * remove non-existant name args in docs * fix typo in message * fix trainees being labeled as queued * add ignore nobles * Remove more debug code * Gui cleanup * Update to use the Military Module * use the squad position * Remove all training dwarves when you disable * disable autotraining on map unload * Apply suggestions from code review Co-authored-by: Christian Doczkal <20443222+chdoc@users.noreply.github.com> * fix erroneous training numbers * Update autotraining.rst * remove outdated debug logging * remove outdated comment * fix up silly code in `removeTraining` * Update autotraining.lua * Update autotraining.lua * clean and de-nest training candidates * remove units who don't need training * use our precomputed good squads list * forgot a nil check * only count as ignored if they are ignored * Apply suggestions from code review Co-authored-by: Christian Doczkal <20443222+chdoc@users.noreply.github.com> * code review changes * Fix up from testing improvements to `autotraining`: - fix the argument error - avoid the double execution of the loop when enabling - consistently only count ignored units when they would otherwise qualify for training - allow enabling the tool from within `gui/autotraining` - sort the list of training candidates, so that the most needed candidates are preferred for training - move the argument handling out of the `start` function Co-Authored-By: Christian Doczkal <20443222+chdoc@users.noreply.github.com> * fix whitespace error * Update and fix changelog * only process cli args if we are running in the cli * skip units in squads (dont mark as ignored tho) --------- Co-authored-by: Christian Doczkal <20443222+chdoc@users.noreply.github.com> --- autotraining.lua | 296 ++++++++++++++++++++++++++++ changelog.txt | 2 + docs/autotraining.rst | 41 ++++ docs/gui/autotraining.rst | 15 ++ gui/autotraining.lua | 264 +++++++++++++++++++++++++ internal/control-panel/registry.lua | 2 + internal/notify/notifications.lua | 18 ++ 7 files changed, 638 insertions(+) create mode 100644 autotraining.lua create mode 100644 docs/autotraining.rst create mode 100644 docs/gui/autotraining.rst create mode 100644 gui/autotraining.lua diff --git a/autotraining.lua b/autotraining.lua new file mode 100644 index 0000000000..5c7df35781 --- /dev/null +++ b/autotraining.lua @@ -0,0 +1,296 @@ +-- Based on the original code by RNGStrategist (who also got some help from Uncle Danny) +--@ enable = true +--@ module = true + +local repeatUtil = require('repeat-util') +local utils=require('utils') + +local GLOBAL_KEY = "autotraining" +local MartialTraining = df.need_type['MartialTraining'] +local ignore_count = 0 + +local function get_default_state() + return { + enabled=false, + threshold=-5000, + ignored={}, + ignored_nobles={}, + training_squads = {}, + } +end + +state = state or get_default_state() + +function isEnabled() + return state.enabled +end + +-- persisting a table with numeric keys results in a json array with a huge number of null entries +-- therefore, we convert the keys to strings for persistence +local function to_persist(persistable) + local persistable_ignored = {} + for k, v in pairs(persistable) do + persistable_ignored[tostring(k)] = v + end + return persistable_ignored +end + +-- loads both from the older array format and the new string table format +local function from_persist(persistable) + if not persistable then + return + end + local ret = {} + for k, v in pairs(persistable) do + ret[tonumber(k)] = v + end + return ret +end + +function persist_state() + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=state.enabled, + threshold=state.threshold, + ignored=to_persist(state.ignored), + ignored_nobles=state.ignored_nobles, + training_squads=to_persist(state.training_squads) + }) +end + +--- Load the saved state of the script +local function load_state() + -- load persistent data + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + state.enabled = persisted_data.enabled or state.enabled + state.threshold = persisted_data.threshold or state.threshold + state.ignored = from_persist(persisted_data.ignored) or state.ignored + state.ignored_nobles = persisted_data.ignored_nobles or state.ignored_nobles + state.training_squads = from_persist(persisted_data.training_squads) or state.training_squads + return state +end + +dfhack.onStateChange[GLOBAL_KEY] = function(sc) + if sc == SC_MAP_UNLOADED then + state.enabled = false + return + end + -- the state changed, is a map loaded and is that map in fort mode? + if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then + -- no its isnt, so bail + return + end + -- yes it was, so: + + -- retrieve state saved in game. merge with default state so config + -- saved from previous versions can pick up newer defaults. + load_state() + if state.enabled then + start() + end + persist_state() +end + + +--###### +--Functions +--###### +local function isIgnoredNoble(unit) + local noblePos = dfhack.units.getNoblePositions(unit) + if noblePos ~= nil then + for _, position in ipairs(noblePos) do + if state.ignored_nobles[position.position.code] then + return true + end + end + end + return false +end + +---@return table +function getTrainingCandidates() + local ret = {} + ignore_count = 0 + for _, unit in ipairs(dfhack.units.getCitizens(true)) do + if not dfhack.units.isAdult(unit) then + goto next_unit + end + local need = getTrainingNeed(unit) + if not need or need.focus_level >= state.threshold then + goto next_unit + end + -- ignored units are those that would like to train but are forbidden from doing so + if state.ignored[unit.id] then + ignore_count = ignore_count + 1 + goto next_unit + end + if isIgnoredNoble(unit) then + ignore_count = ignore_count + 1 + goto next_unit + end + if unit.military.squad_id ~= -1 then + goto next_unit + end + table.insert(ret, { unit = unit, need = need.focus_level }) + ::next_unit:: + end + table.sort(ret, function (a, b) return a.need < b.need end) + return ret +end + +function getTrainingSquads() + local squads = {} + for squad_id, _ in pairs(state.training_squads) do + local squad = df.squad.find(squad_id) + if squad then + table.insert(squads, squad) + else + -- setting to nil during iteration is permitted by lua + state.training_squads[squad_id] = nil + end + end + return squads +end + +function getTrainingNeed(unit) + if unit == nil then return nil end + local needs = unit.status.current_soul.personality.needs + for _, need in ipairs(needs) do + if need.id == MartialTraining then + return need + end + end + return nil +end + +--###### +--Main +--###### + +-- Find all training squads +-- Abort if no squads found +function checkSquads() + local squads = {} + for _, squad in ipairs(getTrainingSquads()) do + if squad.entity_id == df.global.plotinfo.group_id then + local leader = squad.positions[0].occupant + if leader ~= -1 then + table.insert(squads,squad) + end + end + end + + if #squads == 0 then + return nil + end + + return squads +end + +function addTraining(unit,good_squads) + if unit.military.squad_id ~= -1 then + for _, squad in ipairs(good_squads) do + if unit.military.squad_id == squad.id then + return true + end + end + return false + end + for _, squad in ipairs(good_squads) do + for i=1,9,1 do + if squad.positions[i].occupant == -1 then + return dfhack.military.addToSquad(unit.id,squad.id,i) + end + end + end + + return false +end + +function removeAll() + if state.training_squads == nil then return end + for _, squad in ipairs(getTrainingSquads()) do + for i=1,9,1 do + local hf = df.historical_figure.find(squad.positions[i].occupant) + if hf ~= nil then + dfhack.military.removeFromSquad(hf.unit_id) + end + end + end +end + + +function check() + local squads = checkSquads() + local intraining_count = 0 + local inque_count = 0 + if squads == nil then return end + for _,squad in ipairs(squads) do + for i=1,9,1 do + if squad.positions[i].occupant ~= -1 then + local hf = df.historical_figure.find(squad.positions[i].occupant) + if hf ~= nil then + local unit = df.unit.find(hf.unit_id) + local training_need = getTrainingNeed(unit) + if not training_need or training_need.focus_level >= state.threshold then + dfhack.military.removeFromSquad(unit.id) + end + end + end + end + end + for _, p in ipairs(getTrainingCandidates()) do + local added = addTraining(p.unit, squads) + if added then + intraining_count = intraining_count +1 + else + inque_count = inque_count +1 + end + end + print(("%s: %d training, %d waiting, and %d excluded units with training needs"): + format(GLOBAL_KEY, intraining_count, inque_count, ignore_count)) +end + +function start() + repeatUtil.scheduleEvery(GLOBAL_KEY, 1, 'days', check) +end + +function stop() + repeatUtil.cancel(GLOBAL_KEY) +end + +function enable() + state.enabled = true + persist_state() + start() +end + +function disable() + state.enabled = false + persist_state() + stop() + removeAll() +end + +if dfhack_flags.module then + return +end + +validArgs = utils.invert({ + 't' +}) + +local args = utils.processArgs({...}, validArgs) + +if dfhack_flags.enable then + if dfhack_flags.enable_state then + enable() + else + disable() + end +else + -- called on the command-line + if args.t then + state.threshold = 0-tonumber(args.t) + end + print(("autotraining is %s"):format(state.enabled and "enabled" or "disabled")) +end diff --git a/changelog.txt b/changelog.txt index ba3c45c706..d670aa8af2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,8 @@ Template for new versions: # Future ## New Tools +- `autotraining`: new tool to assign citizens to a military squad when they need Martial Training +- `gui/autotraining`: configuration tool for autotraining ## New Features diff --git a/docs/autotraining.rst b/docs/autotraining.rst new file mode 100644 index 0000000000..360f4d08e5 --- /dev/null +++ b/docs/autotraining.rst @@ -0,0 +1,41 @@ +autotraining +============ + +.. dfhack-tool:: + :summary: Assigns citizens to a military squad until they have fulfilled their need for Martial Training + :tags: fort auto bugfix units + +This script automatically assigns citizens with the need for military training to designated training squads. + +You need to have at least one squad that is set up for training. The squad should be set to "Constant Training" in the military screen. The squad doesn't need months off. The members leave the squad once they have satisfied their need for military training. + +The configured uniform determines the skills that are acquired by the training dwarves. Providing "No Uniform" is a perfectly valid choice and will make your militarily inclined civilians become wrestlers over time. However, you can also provide weapons and armor to pre-train civilians for future drafts. + +Once you have made squads for training use `gui/autotraining` to select the squads and ignored units, as well as the needs threshhold. + +Usage +----- + + ``autotraining []`` + +Examples +-------- + +``autotraining`` + Current status of script + +``enable autotraining`` + Checks to see if you have fullfilled the creation of a training squad. + If there is no squad marked for training use, a clickable notification will appear letting you know to set one up/ + Searches your fort for dwarves with a need for military training, and begins assigning them to a training squad. + Once they have fulfilled their need they will be removed from their squad to be replaced by the next dwarf in the list. + +``disable autotraining`` + Stops adding new units to the squad. + +Options +------- + ``-t`` + Use integer values. (Default 5000) + The negative need threshhold to trigger for each citizen + The greater the number the longer before a dwarf is added to the waiting list. diff --git a/docs/gui/autotraining.rst b/docs/gui/autotraining.rst new file mode 100644 index 0000000000..a86b28adf9 --- /dev/null +++ b/docs/gui/autotraining.rst @@ -0,0 +1,15 @@ +gui/autotraining +================ + +.. dfhack-tool:: + :summary: GUI interface for ``autotraining`` + :tags: fort auto interface + +This is an in-game configuration interface for `autotraining`. You can pick squads for training, select ignored units, and set the needs threshold. + +Usage +----- + +:: + + gui/autotraining diff --git a/gui/autotraining.lua b/gui/autotraining.lua new file mode 100644 index 0000000000..a878f5bb95 --- /dev/null +++ b/gui/autotraining.lua @@ -0,0 +1,264 @@ +---@diagnostic disable: missing-fields + +local gui = require('gui') +local widgets = require('gui.widgets') + +local autotraining = reqscript('autotraining') + +local training_squads = autotraining.state.training_squads +local ignored_units = autotraining.state.ignored +local ignored_nobles = autotraining.state.ignored_nobles + +AutoTrain = defclass(AutoTrain, widgets.Window) +AutoTrain.ATTRS { + frame_title='Training Setup', + frame={w=55, h=45}, + resizable=true, -- if resizing makes sense for your dialog + resize_min={w=55, h=20}, -- try to allow users to shrink your windows +} + +local SELECTED_ICON = dfhack.pen.parse{ch=string.char(251), fg=COLOR_LIGHTGREEN} +function AutoTrain:getSquadIcon(squad_id) + if training_squads[squad_id] then + return SELECTED_ICON + end + return nil +end + +function AutoTrain:getSquads() + local squads = {} + for _, squad in ipairs(df.global.world.squads.all) do + if not (squad.entity_id == df.global.plotinfo.group_id) then + goto continue + end + table.insert(squads, { + text = dfhack.translation.translateName(squad.name, true)..(squad.alias ~= '' and ' ('..squad.alias..')' or ''), + icon = self:callback("getSquadIcon", squad.id ), + id = squad.id + }) + + ::continue:: + end + return squads +end + +function AutoTrain:toggleSquad(_, choice) + training_squads[choice.id] = not training_squads[choice.id] + autotraining.persist_state() + self:updateLayout() +end + +local IGNORED_ICON = dfhack.pen.parse{ch='x', fg=COLOR_RED} +function AutoTrain:getUnitIcon(unit_id) + if ignored_units[unit_id] then + return IGNORED_ICON + end + return nil +end + +function AutoTrain:getNobleIcon(noble_code) + if ignored_nobles[noble_code] then + return IGNORED_ICON + end + return nil +end + +function AutoTrain:getUnits() + local unit_choices = {} + for _, unit in ipairs(dfhack.units.getCitizens(true,false)) do + if not dfhack.units.isAdult(unit) then + goto continue + end + + table.insert(unit_choices, { + text = dfhack.units.getReadableName(unit), + icon = self:callback("getUnitIcon", unit.id ), + id = unit.id + }) + ::continue:: + end + return unit_choices +end + +function AutoTrain:toggleUnit(_, choice) + ignored_units[choice.id] = not ignored_units[choice.id] + autotraining.persist_state() + self:updateLayout() +end + +local function to_title_case(str) + return dfhack.capitalizeStringWords(dfhack.lowerCp437(str:gsub('_', ' '))) +end + +function toSet(list) + local set = {} + for _, v in ipairs(list) do + set[v] = true + end + return set +end + +local function add_positions(positions, entity) + if not entity then return end + for _,position in pairs(entity.positions.own) do + positions[position.id] = { + id=position.id+1, + code=position.code, + } + end +end + +function AutoTrain:getPositions() + local positions = {} + local excludedPositions = toSet({ + 'MILITIA_CAPTAIN', + 'MILITIA_COMMANDER', + 'OUTPOST_LIAISON', + 'CAPTAIN_OF_THE_GUARD', + }) + + add_positions(positions, df.historical_entity.find(df.global.plotinfo.civ_id)) + add_positions(positions, df.historical_entity.find(df.global.plotinfo.group_id)) + + -- Step 1: Extract values into a sortable array + local sortedPositions = {} + for _, val in pairs(positions) do + if val and not excludedPositions[val.code] then + table.insert(sortedPositions, val) + end + end + + -- Step 2: Sort the positions (optional, adjust sorting criteria) + table.sort(sortedPositions, function(a, b) + return a.id < b.id -- Sort alphabetically by code + end) + + -- Step 3: Rebuild the table without gaps + positions = {} -- Reset positions table + for i, val in ipairs(sortedPositions) do + positions[i] = { + text = to_title_case(val.code), + value = val.code, + pen = COLOR_LIGHTCYAN, + icon = self:callback("getNobleIcon", val.code), + id = val.id + } + end + + return positions +end + + + +function AutoTrain:toggleNoble(_, choice) + ignored_nobles[choice.value] = not ignored_nobles[choice.value] + autotraining.persist_state() + self:updateLayout() +end + +function AutoTrain:init() + self:addviews{ + widgets.Label{ + frame={ t = 0 , h = 1 }, + text = "Select squads for automatic training:", + }, + widgets.List{ + view_id = "squad_list", + icon_width = 2, + frame = { t = 1, h = 5 }, + choices = self:getSquads(), + on_submit=self:callback("toggleSquad") + }, + widgets.Divider{ frame={t=6, h=1}, frame_style_l = false, frame_style_r = false}, + widgets.Label{ + frame={ t = 7 , h = 1 }, + text = "General options:", + }, + widgets.EditField { + view_id = "threshold", + frame={ t = 8 , h = 1 }, + key = "CUSTOM_T", + label_text = "Need threshold for training: ", + text = tostring(-autotraining.state.threshold), + on_char = function (char, _) + return tonumber(char,10) + end, + on_submit = function (text) + -- still necessary, because on_char does not check pasted text + local entered_number = tonumber(text,10) or 5000 + autotraining.state.threshold = -entered_number + autotraining.persist_state() + -- make sure that the auto correction is reflected in the EditField + self.subviews.threshold:setText(tostring(entered_number)) + end + }, + widgets.ToggleHotkeyLabel { + view_id = 'enable_toggle', + frame = { t = 9, h = 1 }, + label = 'Autotraining is', + key = 'CUSTOM_E', + options = { { value = true, label = 'Enabled', pen = COLOR_GREEN }, + { value = false, label = 'Disabled', pen = COLOR_RED } }, + on_change = function(val) + if val then + autotraining.enable() + else + autotraining.disable() + end + end, + }, + widgets.Divider{ frame={t=10, h=1}, frame_style_l = false, frame_style_r = false}, + widgets.Label{ + frame={ t = 11 , h = 1 }, + text = "Ignored noble positions:", + }, + widgets.List{ + frame = { t = 12 , h = 11}, + view_id = "nobles_list", + icon_width = 2, + choices = self:getPositions(), + on_submit=self:callback("toggleNoble") + }, + widgets.Divider{ frame={t=23, h=1}, frame_style_l = false, frame_style_r = false}, + widgets.Label{ + frame={ t = 24 , h = 1 }, + text = "Select units to exclude from automatic training:" + }, + widgets.FilteredList{ + frame = { t = 25 }, + view_id = "unit_list", + edit_key = "CUSTOM_CTRL_F", + icon_width = 2, + choices = self:getUnits(), + on_submit=self:callback("toggleUnit") + } + } + --self.subviews.unit_list:setChoices(unit_choices) +end + +function AutoTrain:onRenderBody(painter) + self.subviews.enable_toggle:setOption(autotraining.state.enabled) +end + +function AutoTrain:onDismiss() + view = nil +end + +AutoTrainScreen = defclass(AutoTrainScreen, gui.ZScreen) +AutoTrainScreen.ATTRS { + focus_path='autotrain', +} + +function AutoTrainScreen:init() + self:addviews{AutoTrain{}} +end + +function AutoTrainScreen:onDismiss() + view = nil +end + +if not dfhack.world.isFortressMode() or not dfhack.isMapLoaded() then + qerror('gui/autotraining requires a fortress map to be loaded') +end + +view = view and view:raise() or AutoTrainScreen{}:show() diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 76fbee5c10..08b721f8be 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -34,6 +34,8 @@ COMMANDS_BY_IDX = { desc='Automatically shear creatures that are ready for shearing.', params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', 'ShearCreature', ']'}}, {command='autoslab', group='automation', mode='enable'}, + {command='autotraining', group='automation', mode='enable', + desc='Automatically assign units with training needs to training squads. '}, {command='ban-cooking all', group='automation', mode='run'}, {command='buildingplan set boulders false', group='automation', mode='run', desc='Enable if you usually don\'t want to use boulders for construction.'}, diff --git a/internal/notify/notifications.lua b/internal/notify/notifications.lua index 8af7c2c187..653d3887d5 100644 --- a/internal/notify/notifications.lua +++ b/internal/notify/notifications.lua @@ -366,6 +366,24 @@ NOTIFICATIONS_BY_IDX = { dlg.showMessage('Rescue stuck squads', message, COLOR_WHITE) end, }, + { + name='auto_train', + desc='Notifies when there are no squads set up for training', + default=true, + dwarf_fn=function() + local at = reqscript('autotraining') + if (at.isEnabled() and at.checkSquads() == nil) then + return {{text="autotraining: no squads selected",pen=COLOR_LIGHTRED}} + end + end, + on_click=function() + local message = + "You have no squads selected for training.\n".. + "You should have a squad set up to be constantly training with about 8 units needed for training.\n".. + "Then you can select that squad for training in the config.\n\nWould you like to open the config? Alternatively, simply close this popup to go create a squad." + dlg.showYesNoPrompt('Training Squads not configured', message, COLOR_WHITE, function () dfhack.run_command('gui/autotraining') end) + end, + }, { name='traders_ready', desc='Notifies when traders are ready to trade at the depot.', From ebacabdcc2acb2ccd78479eef41522339f67ae2a Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 17 Aug 2025 12:37:52 -0500 Subject: [PATCH 643/811] Update changelog.txt --- changelog.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 4fdc17edf2..24573f22a9 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,11 +27,11 @@ Template for new versions: # Future ## New Tools +- `devel/hello-world`: updated to show off the new Slider widget ## New Features ## Fixes - - `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach ## Misc Improvements @@ -171,7 +171,6 @@ Template for new versions: - `gui/notify`: save reminder changes color to yellow at 30 minutes and to orange at 60 minutes - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete - `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. -- `devel/hello-world`: updated to show off the new Slider widget ## Removed - `gui/create-item`: now accepts a ``pos`` argument of where to spawn items From 56bca449d29e7db991965850984c720a80dc8155 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 17 Aug 2025 12:41:37 -0500 Subject: [PATCH 644/811] Update changelog.txt not sure what happened here --- changelog.txt | 3 --- 1 file changed, 3 deletions(-) diff --git a/changelog.txt b/changelog.txt index 79df95938a..75fd442f29 100644 --- a/changelog.txt +++ b/changelog.txt @@ -172,9 +172,6 @@ Template for new versions: - `gui/notify`: save reminder now appears in adventure mode - `gui/notify`: save reminder changes color to yellow at 30 minutes and to orange at 60 minutes - `gui/confirm`: in the delete manager order confirmation dialog, show a description of which order you have selected to delete -- `position`: display both adventurer and site pos simultaneously. Display map block pos+offset of selected tile. - -## Removed - `gui/create-item`: now accepts a ``pos`` argument of where to spawn items - `modtools/create-item`: exported ``hackWish`` function now supports ``opts.pos`` for determining spawn location - `hfs-pit`: improve placement of stairs w/r/t eerie pits and ramp tops From 0895834b2a78dc3e920af24bbb0df49fa071b6a2 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 17 Aug 2025 13:16:54 -0500 Subject: [PATCH 645/811] Update to use a wrapper function to accept unit OR histfig --- deathcause.lua | 17 +++++++++++++---- docs/deathcause.rst | 9 +++------ 2 files changed, 16 insertions(+), 10 deletions(-) diff --git a/deathcause.lua b/deathcause.lua index 6c212821ac..2da43fca46 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -26,7 +26,7 @@ local function getDeathStringFromCause(cause) end -- Returns a cause of death given a unit -function getDeathCauseFromUnit(unit) +local function getDeathCauseFromUnit(unit) local str = unit.name.has_name and '' or 'The ' str = str .. dfhack.units.getReadableName(unit) @@ -104,7 +104,7 @@ local function getDeathEventForHistFig(histfig_id) end -- Returns the cause of death given a histfig -function getDeathCauseFromHistFig(histfig) +local function getDeathCauseFromHistFig(histfig) local histfig_unit = df.unit.find(histfig.unit_id) if not histfig_unit then qerror("Cause of death not available") @@ -149,6 +149,15 @@ local function get_target() return selected_item.hist_figure_id, df.unit.find(selected_item.unit_id) end +-- wrapper function to take either a unit or a histfig and get the death cause +function getDeathCause(target) + if df.unit:is_instance(target) then + return getDeathCauseFromUnit(target) + else + return getDeathCauseFromHistFig(target) + end +end + if dfhack_flags.module then return end @@ -161,7 +170,7 @@ elseif hist_figure_id == -1 then if not selected_unit then qerror("Cause of death not available") end - print(dfhack.df2console(getDeathCauseFromUnit(selected_unit))) + print(dfhack.df2console(getDeathCause(selected_unit))) else - print(dfhack.df2console(getDeathCauseFromHistFig(df.historical_figure.find(hist_figure_id)))) + print(dfhack.df2console(getDeathCause(df.historical_figure.find(hist_figure_id)))) end diff --git a/docs/deathcause.rst b/docs/deathcause.rst index 20dddb11e6..dac8a39ab9 100644 --- a/docs/deathcause.rst +++ b/docs/deathcause.rst @@ -23,14 +23,11 @@ commandline interface with ``dfhack.run_script()`` or via the API functions defined in :source-scripts:`deathcause.lua`, available from the return value of ``reqscript('deathcause')``: -* ``getDeathCauseFromHistFig(histfig)`` +* ``getDeathCause(unit or historical_figure)`` -Returns a string with the historical figure's cause of death, sometimes with more information -than with a unit. +Returns a string with the unit or historical figure's cause of death. Note that using a historical +figure will sometimes provide more information than using a unit. -* ``getDeathCauseFromUnit(unit)`` - -Returns a string with the unit's cause of death. API usage example:: From eb1cd89483d157a0f16459f33ddbdd3ea73c6b4b Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 17 Aug 2025 13:18:43 -0500 Subject: [PATCH 646/811] write down a note for someone more skilled in lua --- deathcause.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/deathcause.lua b/deathcause.lua index 2da43fca46..3fd62fd115 100644 --- a/deathcause.lua +++ b/deathcause.lua @@ -6,6 +6,7 @@ local DEATH_TYPES = reqscript('gui/unit-info-viewer').DEATH_TYPES -- Gets the first corpse item at the given location local function getItemAtPosition(pos) for _, item in ipairs(df.global.world.items.other.ANY_CORPSE) do + -- could this maybe be `if same_xyz(pos, item.pos) then`? if item.pos.x == pos.x and item.pos.y == pos.y and item.pos.z == pos.z then print("Automatically chose first corpse at the selected location.") return item From 44bf3c69a731006ae1c9d85c389bcc2c99544ea0 Mon Sep 17 00:00:00 2001 From: Squid Coder Date: Sun, 17 Aug 2025 16:29:00 -0500 Subject: [PATCH 647/811] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index 75fd442f29..7204144e7e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,7 +27,6 @@ Template for new versions: # Future ## New Tools -- `devel/hello-world`: updated to show off the new Slider widget - `autotraining`: new tool to assign citizens to a military squad when they need Martial Training - `gui/autotraining`: configuration tool for autotraining @@ -37,6 +36,7 @@ Template for new versions: - `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach ## Misc Improvements +- `devel/hello-world`: updated to show off the new Slider widget ## Removed From 9afb8c058b4e6a0c4c717ddbe779b377cf6f7a93 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 18:32:50 -0500 Subject: [PATCH 648/811] `ban-cooking`: don't fail when honey missing Do not attempt to ban honey if honey doesn't exist --- ban-cooking.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/ban-cooking.lua b/ban-cooking.lua index fdb59fbe05..7ac6f3ea3b 100644 --- a/ban-cooking.lua +++ b/ban-cooking.lua @@ -81,7 +81,9 @@ end funcs.honey = function() local mat = dfhack.matinfo.find("CREATURE:HONEY_BEE:HONEY") - ban_cooking('honey bee honey', mat.type, mat.index, df.item_type.LIQUID_MISC, -1) + if mat then + ban_cooking('honey bee honey', mat.type, mat.index, df.item_type.LIQUID_MISC, -1) + end end funcs.tallow = function() From 6a893937289d287070ab4dfbb3be104aa36abc96 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Sun, 17 Aug 2025 18:36:48 -0500 Subject: [PATCH 649/811] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 89fd20ca8a..6eeaffd48e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -37,6 +37,7 @@ Template for new versions: - `deathcause`: added functionality to this script to fetch cause of death programatically ## Fixes +- `ban-cooking`: will not fail trying to ban honey if the world has no honey - `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach ## Misc Improvements From 5290750c5a233c8589a7f4645f76af6f57f964e5 Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Tue, 19 Aug 2025 09:35:20 -0500 Subject: [PATCH 650/811] fix enable/disable in husbandry --- husbandry.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/husbandry.lua b/husbandry.lua index f8dd1375ec..a5a8bff91a 100644 --- a/husbandry.lua +++ b/husbandry.lua @@ -272,10 +272,10 @@ end if dfhack_flags.enable then if dfhack_flags.enable_state then - enabled = true + state.enabled = true start() else - enabled = false + state.enabled = false stop() end persist_state() From e4ac14bd29b1fd5de26612aa16be40f4ba3ecc48 Mon Sep 17 00:00:00 2001 From: SilasD Date: Tue, 19 Aug 2025 09:05:10 -0700 Subject: [PATCH 651/811] changelog.txt update --- changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/changelog.txt b/changelog.txt index a5a7b06f0f..c25345a07a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -46,6 +46,8 @@ Template for new versions: - `confirm`: the pause option now pauses individual confirmation types, allowing multiple different confirmations to be paused independently - `immortal-cravings`: prioritize high-value meals, properly split of portions, and don't go eating or drinking on a full stomach - `uniform-unstick`: no longer causes units to equip multiples of assigned items +- `caravan`: in the pedestal item assignment dialog, add new items at the end of the list of displayed items instead of at a random position +- `caravan`: in the pedestal item assignment dialog, consistently remove items from the list of displayed items ## Misc Improvements - `devel/hello-world`: updated to show off the new Slider widget From 929e66147805da9fb3a29283b05812ceb68d1d1b Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 21 Aug 2025 18:51:26 +0200 Subject: [PATCH 652/811] fix index of nil value --- husbandry.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/husbandry.lua b/husbandry.lua index a5a8bff91a..cf80857468 100644 --- a/husbandry.lua +++ b/husbandry.lua @@ -116,7 +116,7 @@ local function getAppropriateWorkshop(unit, collection) end end end - return #closest.jobs < 10 and closest or nil + return (closest and #closest.jobs < 10) and closest or nil end local function shearCreature(unit, workshop) From 539120522a808220e4ab23538bad5cfcd979211e Mon Sep 17 00:00:00 2001 From: Kelly Kinkade Date: Fri, 22 Aug 2025 00:33:04 -0500 Subject: [PATCH 653/811] changelog for 52.03-r2 --- changelog.txt | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/changelog.txt b/changelog.txt index c25345a07a..5ecf8024e1 100644 --- a/changelog.txt +++ b/changelog.txt @@ -26,6 +26,18 @@ Template for new versions: # Future +## New Tools + +## New Features + +## Fixes + +## Misc Improvements + +## Removed + +# 52.03-r2 + ## New Tools - `autotraining`: new tool to assign citizens to a military squad when they need Martial Training - `gui/autotraining`: configuration tool for autotraining From ab665c18d8c91fc8690b1b526ce21951456ae8b5 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 21 Aug 2025 17:19:07 +0200 Subject: [PATCH 654/811] adapt Lua tools to use new API functionality for creating and assigning jobs --- autocheese.lua | 32 ++++++--------------- changelog.txt | 3 ++ husbandry.lua | 9 +++--- idle-crafting.lua | 66 +++++++++---------------------------------- immortal-cravings.lua | 46 ++++++------------------------ 5 files changed, 38 insertions(+), 118 deletions(-) diff --git a/autocheese.lua b/autocheese.lua index 0e9fb52215..e9bdc146ae 100644 --- a/autocheese.lua +++ b/autocheese.lua @@ -1,14 +1,12 @@ --@module = true -local ic = reqscript('idle-crafting') - ---make cheese using a specific barrel and workshop ---@param barrel df.item ---@param workshop df.building_workshopst ---@return df.job function makeCheese(barrel, workshop) ---@type df.job - local job = ic.make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeCheese local jitem = df.job_item:new() @@ -22,29 +20,17 @@ function makeCheese(barrel, workshop) dfhack.error('could not attach item') end - ic.assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return job end - - ----unit is ready to take jobs +---checks that unit can path to workshop ---@param unit df.unit +---@param workshop df.building_workshopst ---@return boolean -function unitIsAvailable(unit) - if unit.job.current_job then - return false - elseif #unit.individual_drills > 0 then - return false - elseif unit.flags1.caged or unit.flags1.chained then - return false - elseif unit.military.squad_id ~= -1 then - local squad = df.squad.find(unit.military.squad_id) - -- this lookup should never fail - ---@diagnostic disable-next-line: need-check-nil - return #squad.orders == 0 and squad.activity == -1 - end - return true +function canAccessWorkshop(unit, workshop) + local workshop_position = xyz2pos(workshop.centerx, workshop.centery, workshop.z) + return dfhack.maps.canWalkBetween(unit.pos, workshop_position) end ---check if unit can perform labor at workshop @@ -54,8 +40,8 @@ end ---@return boolean function availableLaborer(unit, unit_labor, workshop) return unit.status.labors[unit_labor] - and unitIsAvailable(unit) - and ic.canAccessWorkshop(unit, workshop) + and dfhack.units.isJobAvailable(unit) + and canAccessWorkshop(unit, workshop) end ---find unit with a particular labor enabled diff --git a/changelog.txt b/changelog.txt index 5ecf8024e1..6fc58b711b 100644 --- a/changelog.txt +++ b/changelog.txt @@ -34,6 +34,9 @@ Template for new versions: ## Misc Improvements +- adapt Lua tools to use new API functionality for creating and assigning jobs +- `idle-crafting`: properly interrupt interruptible (i.e. "green") social activities + ## Removed # 52.03-r2 diff --git a/husbandry.lua b/husbandry.lua index cf80857468..a80e5b2417 100644 --- a/husbandry.lua +++ b/husbandry.lua @@ -4,7 +4,6 @@ local utils = require 'utils' local repeatutil = require("repeat-util") -local ic = reqscript('idle-crafting') local verbose = true ---conditional printing of debug messages @@ -120,17 +119,17 @@ local function getAppropriateWorkshop(unit, collection) end local function shearCreature(unit, workshop) - local job = ic.make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.ShearCreature dfhack.job.addGeneralRef(job, df.general_ref_type.UNIT_SHEAREE, unit.id) - ic.assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) end local function milkCreature(unit, workshop) - local job = ic.make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MilkCreature dfhack.job.addGeneralRef(job, df.general_ref_type.UNIT_MILKEE, unit.id) - ic.assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) end diff --git a/idle-crafting.lua b/idle-crafting.lua index 6744f02fde..e5d25751bf 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -55,26 +55,12 @@ function weightedChoice(choices) return nil --never reached on well-formed input end ----create a new linked job ----@return df.job -function make_job() - local job = df.job:new() - dfhack.job.linkIntoWorld(job, true) - return job -end - -function assignToWorkshop(job, workshop) - job.pos = xyz2pos(workshop.centerx, workshop.centery, workshop.z) - dfhack.job.addGeneralRef(job, df.general_ref_type.BUILDING_HOLDER, workshop.id) - workshop.jobs:insert("#", job) -end - ---make totem at specified workshop ---@param unit df.unit ---@param workshop df.building_workshopst ---@return boolean function makeTotem(unit, workshop) - local job = make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeTotem job.mat_type = -1 @@ -89,7 +75,7 @@ function makeTotem(unit, workshop) jitem.flags2.body_part = true job.job_items.elements:insert('#', jitem) - assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end @@ -98,7 +84,7 @@ end ---@param workshop df.building_workshopst ---@return boolean function makeHornCrafts(unit, workshop) - local job = make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeCrafts job.mat_type = -1 job.material_category.horn = true @@ -114,7 +100,7 @@ function makeHornCrafts(unit, workshop) jitem.flags2.body_part = true job.job_items.elements:insert('#', jitem) - assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end @@ -123,7 +109,7 @@ end ---@param workshop df.building_workshopst ---@return boolean function makeBoneCraft(unit, workshop) - local job = make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeCrafts job.mat_type = -1 job.material_category.bone = true @@ -139,7 +125,7 @@ function makeBoneCraft(unit, workshop) jitem.flags2.body_part = true job.job_items.elements:insert('#', jitem) - assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end @@ -148,7 +134,7 @@ end ---@param workshop df.building_workshopst ---@return boolean function makeShellCraft(unit, workshop) - local job = make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeCrafts job.mat_type = -1 job.material_category.shell = true @@ -164,7 +150,7 @@ function makeShellCraft(unit, workshop) jitem.flags2.body_part = true job.job_items.elements:insert('#', jitem) - assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end @@ -173,7 +159,7 @@ end ---@param workshop df.building_workshopst ---@return boolean "" function makeRockCraft(unit, workshop) - local job = make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.MakeCrafts job.mat_type = 0 @@ -187,7 +173,7 @@ function makeRockCraft(unit, workshop) jitem.flags3.hard = true job.job_items.elements:insert('#', jitem) - assignToWorkshop(job, workshop) + dfhack.job.assignToWorkshop(job, workshop) return dfhack.job.addWorker(job, unit) end @@ -291,13 +277,8 @@ local STONE_CRAFT = df.unit_labor['STONE_CRAFT'] ---@param value_if_absent T ---@return number|T function getCraftingNeed(unit, value_if_absent) - local needs = unit.status.current_soul.personality.needs - for _, need in ipairs(needs) do - if need.id == CraftObject then - return -need.focus_level - end - end - return value_if_absent + local focus_penalty = dfhack.units.getFocusPenalty(unit, CraftObject) + return focus_penalty > 1000 and value_if_absent or -focus_penalty end local function stop() @@ -334,27 +315,6 @@ function canAccessWorkshop(unit, workshop) return dfhack.maps.canWalkBetween(unit.pos, workshop_position) end ----unit is ready to take jobs ----@param unit df.unit ----@return boolean -function unitIsAvailable(unit) - if unit.job.current_job then - return false - elseif #unit.specific_refs > 0 then -- activities such as "Conduct Meeting" - return false - elseif #unit.social_activities > 0 then - return false - elseif #unit.individual_drills > 0 then - return false - elseif unit.military.squad_id ~= -1 then - local squad = df.squad.find(unit.military.squad_id) - -- this lookup should never fail - ---@diagnostic disable-next-line: need-check-nil - return #squad.orders == 0 and squad.activity == -1 - end - return true -end - ---select crafting job based on available resources ---@param workshop df.building_workshopst ---@return (fun(unit:df.unit, workshop:df.building_workshopst):boolean)? @@ -397,7 +357,7 @@ local function processUnit(workshop, idx, unit_id) elseif not canAccessWorkshop(unit, workshop) then -- dfhack.print('-') return false - elseif not unitIsAvailable(unit) then + elseif not dfhack.units.isJobAvailable(unit) then -- dfhack.print('.') return false end diff --git a/immortal-cravings.lua b/immortal-cravings.lua index 21ae333ecb..e08e072ca5 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -1,12 +1,11 @@ --@enable = true --@module = true -local idle = reqscript('idle-crafting') local repeatutil = require("repeat-util") --- utility functions -local verbose = false +local verbose = true ---conditional printing of debug messages ---@param message string local function debug(message) @@ -101,7 +100,7 @@ local function goDrink(unit) -- print('no accessible drink found') return end - local job = idle.make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.DrinkItem job.flags.special = true local dx, dy, dz = dfhack.items.getPosition(drink) @@ -134,7 +133,7 @@ local function goEat(unit) end dfhack.items.setOwner(meal, unit) - local job = idle.make_job() + local job = dfhack.job.createLinked() job.job_type = df.job_type.Eat job.flags.special = true local dx, dy, dz = dfhack.items.getPosition(meal) @@ -148,25 +147,6 @@ local function goEat(unit) print(dfhack.df2console('immortal-cravings: %s is getting something to eat'):format(name)) end ----unit is ready to take jobs (will interrupt social activities) ----@param unit df.unit ----@return boolean -function unitIsAvailable(unit) - if unit.job.current_job then - return false - elseif #unit.individual_drills > 0 then - return false - elseif unit.flags1.caged or unit.flags1.chained then - return false - elseif unit.military.squad_id ~= -1 then - local squad = df.squad.find(unit.military.squad_id) - -- this lookup should never fail - ---@diagnostic disable-next-line: need-check-nil - return #squad.orders == 0 and squad.activity == -1 - end - return true -end - --- script logic local GLOBAL_KEY = 'immortal-cravings' @@ -210,7 +190,7 @@ local function unit_loop() then goto next_unit end - if not unitIsAvailable(unit) then + if not dfhack.units.isJobAvailable(unit) then debug("immortal-cravings: skipping busy"..dfhack.units.getReadableName(unit)) table.insert(kept, unit.id) else @@ -245,21 +225,13 @@ local function main_loop() watched = {} for _, unit in ipairs(dfhack.units.getCitizens(false, false)) do if - not (is_active_caste_flag(unit, 'NO_DRINK') or is_active_caste_flag(unit, 'NO_EAT')) or - unit.counters2.stomach_content > 0 + (is_active_caste_flag(unit, 'NO_DRINK') or is_active_caste_flag(unit, 'NO_EAT')) and + unit.counters2.stomach_content == 0 and + dfhack.units.getFocusPenalty(unit, DrinkAlcohol, EatGoodMeal) < threshold then - goto next_unit - end - for _, need in ipairs(unit.status.current_soul.personality.needs) do - if need.id == DrinkAlcohol and need.focus_level < threshold or - need.id == EatGoodMeal and need.focus_level < threshold - then - table.insert(watched, unit.id) - debug(' '..dfhack.df2console(dfhack.units.getReadableName(unit))) - goto next_unit - end + table.insert(watched, unit.id) + debug(' ' .. dfhack.df2console(dfhack.units.getReadableName(unit))) end - ::next_unit:: end if #watched > 0 then From 8ff279e3052c026ba9f526a174be004e39c7d7a6 Mon Sep 17 00:00:00 2001 From: Christian Doczkal <20443222+chdoc@users.noreply.github.com> Date: Thu, 21 Aug 2025 21:21:05 +0200 Subject: [PATCH 655/811] reduce verbosity --- husbandry.lua | 2 +- immortal-cravings.lua | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/husbandry.lua b/husbandry.lua index a80e5b2417..9198013a1f 100644 --- a/husbandry.lua +++ b/husbandry.lua @@ -5,7 +5,7 @@ local utils = require 'utils' local repeatutil = require("repeat-util") -local verbose = true +local verbose = false ---conditional printing of debug messages ---@param message string local function debug(message) diff --git a/immortal-cravings.lua b/immortal-cravings.lua index e08e072ca5..da9f90d76c 100644 --- a/immortal-cravings.lua +++ b/immortal-cravings.lua @@ -5,7 +5,7 @@ local repeatutil = require("repeat-util") --- utility functions -local verbose = true +local verbose = false ---conditional printing of debug messages ---@param message string local function debug(message) From ff200329647fd504a939c09ae13f7d335b6d411b Mon Sep 17 00:00:00 2001 From: Droseran <97368320+Droseran@users.noreply.github.com> Date: Sun, 24 Aug 2025 11:04:55 -0400 Subject: [PATCH 656/811] Support honey added by mods Instead of only banning honey from vanilla honey bees, support banning honey added by modded creatures as well. --- ban-cooking.lua | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/ban-cooking.lua b/ban-cooking.lua index 7ac6f3ea3b..2254e116dd 100644 --- a/ban-cooking.lua +++ b/ban-cooking.lua @@ -80,9 +80,18 @@ funcs.booze = function() end funcs.honey = function() - local mat = dfhack.matinfo.find("CREATURE:HONEY_BEE:HONEY") - if mat then - ban_cooking('honey bee honey', mat.type, mat.index, df.item_type.LIQUID_MISC, -1) + for _, c in ipairs(df.global.world.raws.creatures.all) do + for _, m in ipairs(c.material) do + if m.flags.EDIBLE_COOKED then + for _, s in ipairs(m.reaction_product.id) do + if s.value == "DRINK_MAT" then + local matinfo = dfhack.matinfo.find(c.creature_id, m.id) + ban_cooking(c.name[2] .. ' ' .. m.id, matinfo.type, matinfo.index, df.item_type.LIQUID_MISC, -1) + break + end + end + end + end end end From 2f5aa107788b5fc1bbfbb5138d87ab14390ec659 Mon Sep 17 00:00:00 2001 From: Droseran <97368320+Droseran@users.noreply.github.com> Date: Sun, 24 Aug 2025 11:18:16 -0400 Subject: [PATCH 657/811] Update changelog.txt --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 5ecf8024e1..c22c4957a4 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,6 +25,7 @@ Template for new versions: ]]] # Future +- `ban-cooking`: bans honey added by creatures other than vanilla honey bee ## New Tools From 18fa50477d40960f6f9aee08f1a8da922b866362 Mon Sep 17 00:00:00 2001 From: Droseran <97368320+Droseran@users.noreply.github.com> Date: Sun, 24 Aug 2025 11:30:05 -0400 Subject: [PATCH 658/811] Update changelog.txt --- changelog.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/changelog.txt b/changelog.txt index c22c4957a4..e2d0e02107 100644 --- a/changelog.txt +++ b/changelog.txt @@ -25,13 +25,13 @@ Template for new versions: ]]] # Future -- `ban-cooking`: bans honey added by creatures other than vanilla honey bee ## New Tools ## New Features ## Fixes +- `ban-cooking`: bans honey added by creatures other than vanilla honey bee ## Misc Improvements From 0e3c7edebfce7ec8d10e921d694816415fc752bd Mon Sep 17 00:00:00 2001 From: SilasD Date: Tue, 26 Aug 2025 12:18:07 -0700 Subject: [PATCH 659/811] =?UTF-8?q?internal/caravan/*=20allow=20searching?= =?UTF-8?q?=20for=20items=20with=20CP417=20names.=20Such=20as:=20=20=20hye?= =?UTF-8?q?na=20bone=20figurine=20of=20B=C3=ABr=C3=BBl=20Saviorstockade=20?= =?UTF-8?q?=20=20L=C3=A2ven=20=C3=B4sed,=20The=20Prairie=20of=20Mazes=20(S?= =?UTF-8?q?hield)=20=20=20(+=C2=ABgrown=20pear=20wood=20=C3=AD=C3=BF=C3=AD?= =?UTF-8?q?mo=C2=BB+)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- changelog.txt | 1 + internal/caravan/common.lua | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/changelog.txt b/changelog.txt index 5ecf8024e1..e694a6371a 100644 --- a/changelog.txt +++ b/changelog.txt @@ -33,6 +33,7 @@ Template for new versions: ## Fixes ## Misc Improvements +- `caravan`: the ``Bring goods to depot``, ``Trade``, and ``Assign items for display`` screens now allow searching for items with non-ASCII characters in their description ## Removed diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 996a616562..f3ba1fa9ca 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -15,8 +15,8 @@ SOME_PEN = to_pen{ch=':', fg=COLOR_YELLOW} ALL_PEN = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN} function add_words(words, str) - for word in str:gmatch("[%w]+") do - table.insert(words, word:lower()) + for word in dfhack.toSearchNormalized(str):gmatch("[%w]+") do + table.insert(words, word) end end From dbd125b3bb16022e40222587098e0f603a71d621 Mon Sep 17 00:00:00 2001 From: SilasD Date: Tue, 26 Aug 2025 14:20:10 -0700 Subject: [PATCH 660/811] internal/caravan/common.lua obfuscate_value() API change An optional parameter `threshold` was added to allow the caller to pass in that value instead of recalculating it on each call. `get_broker_skill()` is slow, and `obfuscate_value()` is typically called 1000s of times, so passing `threshold` improves performance. This API change maintains backwards compatiblity. In addition, `get_broker_skill()` and `get_threshold()` were exposed to scripts that use this module. Also minor code cleanup: * An alias was only used twice, right after it was defined. The code is better off written without the alias. * Integers should ideally be compared with integers. No user-visible changes. --- internal/caravan/common.lua | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index f3ba1fa9ca..8f22306261 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -10,9 +10,8 @@ CH_DN = string.char(31) CH_MONEY = string.char(15) CH_EXCEPTIONAL = string.char(240) -local to_pen = dfhack.pen.parse -SOME_PEN = to_pen{ch=':', fg=COLOR_YELLOW} -ALL_PEN = to_pen{ch=string.char(251), fg=COLOR_LIGHTGREEN} +SOME_PEN = dfhack.pen.parse{ch=':', fg=COLOR_YELLOW} +ALL_PEN = dfhack.pen.parse{ch=string.char(251), fg=COLOR_LIGHTGREEN} function add_words(words, str) for word in dfhack.toSearchNormalized(str):gmatch("[%w]+") do @@ -35,7 +34,7 @@ function make_container_search_key(item, desc) return table.concat(words, ' ') end -local function get_broker_skill() +function get_broker_skill() local broker = dfhack.units.getUnitByNobleRole('broker') if not broker then return 0 end for _,skill in ipairs(broker.status.current_soul.skills) do @@ -46,7 +45,7 @@ local function get_broker_skill() return 0 end -local function get_threshold(broker_skill) +function get_threshold(broker_skill) if broker_skill <= df.skill_rating.Dabbling then return 0 end if broker_skill <= df.skill_rating.Novice then return 10 end if broker_skill <= df.skill_rating.Adequate then return 25 end @@ -62,7 +61,7 @@ local function get_threshold(broker_skill) if broker_skill <= df.skill_rating.Master then return 4000 end if broker_skill <= df.skill_rating.HighMaster then return 5000 end if broker_skill <= df.skill_rating.GrandMaster then return 10000 end - return math.huge + return math.maxinteger end local function estimate(value, round_base, granularity) @@ -76,8 +75,8 @@ end -- Otherwise, if it's less than or equal to [threshold + 50] * 3, it will round to the nearest multiple of 100 -- Otherwise, if it's less than or equal to [threshold + 50] * 30, it will round to the nearest multiple of 1000 -- Otherwise, it will display a guess equal to [threshold + 50] * 30 rounded up to the nearest multiple of 1000. -function obfuscate_value(value) - local threshold = get_threshold(get_broker_skill()) +function obfuscate_value(value, threshold) + threshold = threshold or get_threshold(get_broker_skill()) if value < threshold then return dfhack.formatInt(value) end threshold = threshold + 50 if value <= threshold then return ('~%s'):format(estimate(value, 5, 10)) end @@ -267,7 +266,7 @@ function get_slider_widgets(self, suffix) {label='100'..CH_MONEY, value={index=4, value=100}, pen=COLOR_BROWN}, {label='500'..CH_MONEY, value={index=5, value=500}, pen=COLOR_BROWN}, {label='1000'..CH_MONEY, value={index=6, value=1000}, pen=COLOR_BROWN}, - {label='Max', value={index=7, value=math.huge}, pen=COLOR_GREEN}, + {label='Max', value={index=7, value=math.maxinteger}, pen=COLOR_GREEN}, }, initial_option=7, on_change=function(val) From 761a677c9843f0450e349b20a69483c79257bf29 Mon Sep 17 00:00:00 2001 From: SilasD Date: Tue, 26 Aug 2025 18:48:38 -0700 Subject: [PATCH 661/811] internal/caravan/movegoods.lua use new obfuscate_value() API Verified that this gives identical results to the unmodified code, with the exception of some icon closures that could not be verified. --- internal/caravan/movegoods.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/internal/caravan/movegoods.lua b/internal/caravan/movegoods.lua index df90d7f886..77e8222979 100644 --- a/internal/caravan/movegoods.lua +++ b/internal/caravan/movegoods.lua @@ -399,10 +399,10 @@ local function get_entry_icon(data, item_id) return common.SOME_PEN end -local function make_choice_text(at_depot, dist, value, quantity, desc) +local function make_choice_text(at_depot, dist, value, quantity, desc, cache_threshold) return { {width=DIST_COL_WIDTH-2, rjustify=true, text=at_depot and 'depot' or tostring(dist)}, - {gap=2, width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value)}, + {gap=2, width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value, cache_threshold)}, {gap=2, width=QTY_COL_WIDTH, rjustify=true, text=quantity}, {gap=2, text=desc}, } @@ -559,18 +559,20 @@ function MoveGoods:cache_choices() end local group_choices, nogroup_choices = {}, {} + local cache_threshold = common.get_threshold(common.get_broker_skill()) for _, group in pairs(groups) do local data = group.data for item_id, item_data in pairs(data.items) do local nogroup_choice = copyall(group) nogroup_choice.icon = curry(get_entry_icon, data, item_id) nogroup_choice.text = make_choice_text(item_data.item.flags.in_building, - data.dist, data.per_item_value, 1, data.desc) + data.dist, data.per_item_value, 1, data.desc, cache_threshold) nogroup_choice.item_id = item_id table.insert(nogroup_choices, nogroup_choice) end data.total_value = data.per_item_value * data.quantity - group.text = make_choice_text(data.num_at_depot == data.quantity, data.dist, data.total_value, data.quantity, data.desc) + group.text = make_choice_text(data.num_at_depot == data.quantity, data.dist, + data.total_value, data.quantity, data.desc, cache_threshold) table.insert(group_choices, group) self.value_pending = self.value_pending + (data.per_item_value * data.selected) end From de12a181b1cd4bb96d0403423dd499c7461d80fd Mon Sep 17 00:00:00 2001 From: SilasD Date: Tue, 26 Aug 2025 21:18:22 -0700 Subject: [PATCH 662/811] internal/caravan/pedestal.lua use new obfuscate_value() API Verified that this gives identical results to the unmodified code. --- internal/caravan/pedestal.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index dab100ef48..1184a85c3e 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -503,10 +503,10 @@ local function get_status(item, display_bld) return STATUS.AVAILABLE.value end -local function make_choice_text(data) +local function make_choice_text(data, threshold) return { {width=STATUS_COL_WIDTH, text=function() return STATUS[STATUS_REVMAP[data.status]].label end}, - {gap=2, width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(data.value)}, + {gap=2, width=VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(data.value, threshold)}, {gap=2, text=data.desc}, } end @@ -530,6 +530,7 @@ end function AssignItems:cache_choices(inside_containers, display_bld) if self.choices_cache[inside_containers] then return self.choices_cache[inside_containers] end + local cache_threshold = common.get_threshold(common.get_broker_skill()) local choices = {} for _, item in ipairs(df.global.world.items.other.IN_PLAY) do if not is_displayable_item(item) then goto continue end @@ -559,7 +560,7 @@ function AssignItems:cache_choices(inside_containers, display_bld) end local entry = { search_key=search_key, - text=make_choice_text(data), + text=make_choice_text(data, cache_threshold), data=data, } table.insert(choices, entry) From 76b0f7e4e029613da49f0994b712a00e479c9a6b Mon Sep 17 00:00:00 2001 From: SilasD Date: Wed, 27 Aug 2025 07:44:12 -0700 Subject: [PATCH 663/811] internal/caravan/trade.lua use new obfuscate_value() API Verified that this gives identical results to the unmodified code. --- internal/caravan/trade.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/internal/caravan/trade.lua b/internal/caravan/trade.lua index f27e949ea5..f78bc75c05 100644 --- a/internal/caravan/trade.lua +++ b/internal/caravan/trade.lua @@ -315,9 +315,9 @@ local function is_ethical_product(item, animal_ethics, wood_ethics) (not wood_ethics or not common.has_wood(item)) end -local function make_choice_text(value, desc) +local function make_choice_text(value, threshold, desc) return { - {width=STATUS_COL_WIDTH+VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value)}, + {width=STATUS_COL_WIDTH+VALUE_COL_WIDTH, rjustify=true, text=common.obfuscate_value(value, threshold)}, {gap=2, text=desc}, } end @@ -328,6 +328,7 @@ function Trade:cache_choices(list_idx, trade_bins) local goodflags = trade.goodflag[list_idx] local trade_bins_choices, notrade_bins_choices = {}, {} local parent_data + local cache_threshold = common.get_threshold(common.get_broker_skill()) for item_idx, item in ipairs(trade.good[list_idx]) do local goodflag = goodflags[item_idx] if not goodflag.contained then @@ -374,7 +375,7 @@ function Trade:cache_choices(list_idx, trade_bins) search_key=search_key, icon=curry(get_entry_icon, data), data=data, - text=make_choice_text(data.value, desc), + text=make_choice_text(data.value, cache_threshold, desc), } if not data.update_container_fn then table.insert(trade_bins_choices, choice) From 4e93bee0647c5b3ce121d6b716824896adee713e Mon Sep 17 00:00:00 2001 From: SilasD Date: Thu, 28 Aug 2025 09:59:51 -0700 Subject: [PATCH 664/811] internal/caravan/common.lua use trader over broker When doing trading (i.e. the DF Trade window is open showing the two columns), it *can* happen that you *have* a broker, but you actually *trade* using a different unit. This can be triggered by opening the depot building view and choosing `Anyone requested at trade`, repeating this until some unit that is not the broker shows up to do the trading. When this happens, the DFHack `Select trade goods` overlay shows different obfuscated values than the DF Trade window. This patch fixes that case by using the trader's appraisal skill if trading is active. --- internal/caravan/common.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 8f22306261..9fffefe4c6 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -36,6 +36,13 @@ end function get_broker_skill() local broker = dfhack.units.getUnitByNobleRole('broker') + local interface_trade = df.global.game.main_interface.trade + if interface_trade.open == true + and interface_trade.choosing_merchant == false + and interface_trade.fortress_trader ~= nil + then + broker = interface_trade.fortress_trader + end if not broker then return 0 end for _,skill in ipairs(broker.status.current_soul.skills) do if skill.id == df.job_skill.APPRAISAL then From f07ef322e293f0fcab6c0095d2c04370f39afe78 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 19:38:33 +0000 Subject: [PATCH 665/811] [pre-commit.ci] pre-commit autoupdate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit updates: - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) - [github.com/python-jsonschema/check-jsonschema: 0.33.2 → 0.33.3](https://github.com/python-jsonschema/check-jsonschema/compare/0.33.2...0.33.3) - [github.com/pre-commit/pre-commit-hooks: v5.0.0 → v6.0.0](https://github.com/pre-commit/pre-commit-hooks/compare/v5.0.0...v6.0.0) --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ec6f9ff9f..21c674fe15 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ ci: repos: # shared across repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: check-added-large-files - id: check-case-conflict @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.33.2 + rev: 0.33.3 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks @@ -34,6 +34,6 @@ repos: - json # specific to scripts: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: forbid-new-submodules From 86456fa1f95c4b5a6bfbb644e27216540b82e71c Mon Sep 17 00:00:00 2001 From: git--amade Date: Sun, 7 Sep 2025 00:41:26 +0800 Subject: [PATCH 666/811] new script store-owned.lua and its documentation --- changelog.txt | 1 + docs/store-owned.rst | 43 +++++ store-owned.lua | 410 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 454 insertions(+) create mode 100644 docs/store-owned.rst create mode 100644 store-owned.lua diff --git a/changelog.txt b/changelog.txt index 5ecf8024e1..9d10f6fe84 100644 --- a/changelog.txt +++ b/changelog.txt @@ -27,6 +27,7 @@ Template for new versions: # Future ## New Tools +- `store-owned`: task owned items to be stored in the owner's room furniture ## New Features diff --git a/docs/store-owned.rst b/docs/store-owned.rst new file mode 100644 index 0000000000..3fedc74336 --- /dev/null +++ b/docs/store-owned.rst @@ -0,0 +1,43 @@ +store-owned +=========== + +.. dfhack-tool:: + :summary: Task units to store their owned items. + :tags: fort items buildings + +Task any owned item to be stored in an appropriate storage furniture in +a room assigned to the item's owner. + +Usage +----- + +``store-owned [