From eca2510ce38548d9238510f93fe4383de8a5cde9 Mon Sep 17 00:00:00 2001 From: woutersparre <63191859+woutersparre@users.noreply.github.com> Date: Wed, 17 Dec 2025 20:16:33 +0100 Subject: [PATCH 1/3] Petitions Overlay Script that add extra information about petitioners to the petitions screen --- petitioners.lua | 232 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 232 insertions(+) create mode 100644 petitioners.lua diff --git a/petitioners.lua b/petitioners.lua new file mode 100644 index 000000000..18ba22132 --- /dev/null +++ b/petitioners.lua @@ -0,0 +1,232 @@ +--@ module = true +-- This script defines a DFHack overlay widget that augments the Petitioners screen +-- by showing additional information about the units involved in a selected petition. + +local gui = require('gui') -- GUI helpers (pens, frames, layout) +local overlay = require('plugins.overlay') -- Overlay framework (OverlayWidget base) +local widgets = require('gui.widgets') -- Standard DFHack UI widgets (List, Panel, Label, etc.) +local utils = require('utils') -- Misc DFHack utilities + +-- ------------------- +-- Utility functions +-- ------------------- + +-- Determines which petition row is currently selected in the vanilla UI +-- by comparing screen texture positions. This is a heuristic that tracks +-- which row's screen texpos matches the initially captured value. +function getActivePetitionRow(self) + local starty = 6 -- Y offset where the first petition row starts + local steps = 3 -- Vertical spacing between petition rows + local listlength = #df.global.plotinfo.petitions -- Number of petitions currently listed + local gps = df.global.gps -- Global screen state (texture positions, dimensions) + if not gps then + return nil -- If GPS is unavailable, we cannot determine selection + end + + -- Capture the initially selected texpos once. This serves as the reference + -- value that identifies the currently highlighted row. + if self.tocheck == nil or self.tocheck == 0 then + self.tocheck = gps.screentexpos_lower[6 * gps.dimy + starty] or 0 + --print("tocheck " .. self.tocheck) + end + + -- Iterate over all visible petition rows and compare their texpos + -- against the captured reference. The matching row index is returned. + for i = 0, listlength - 1 do + local y = starty + i * steps + local idx = (6 * gps.dimy) + y + local tex = gps.screentexpos_lower[idx] or 0 + + if tex == self.tocheck then + return i -- Found the active petition row + end + end + return nil +end + +-- Helper that returns a localized caste/profession name for a unit +local function get_caste_name(race, caste, profession) + return dfhack.units.getCasteProfessionName(race, caste, profession) +end + +-- ------------------- +-- PetitionersOverlay +-- ------------------- + +-- Define a new overlay widget that attaches to the Petitioners screen +PetitionersOverlay = defclass(PetitionersOverlay, overlay.OverlayWidget) +PetitionersOverlay.ATTRS{ + desc="Add information about the petitioners to the Petition screen", -- Short description + default_enabled=true, -- Overlay is enabled by default + version=3, -- Config version for migration/reset + viewscreens={'dwarfmode/Petitions'}, -- Only active on the Petitions screen + frame_background=gui.CLEAR_PEN, -- Transparent background +} + +-- Initialization runs once when the widget is created +function PetitionersOverlay:init() + self.firstrender = true -- Flag to perform one-time setup on first render + self.tocheck = nil -- Stored texpos reference for row detection + self.last_petitions_size = #df.global.plotinfo.petitions -- Track petition count + self.last_selected_petition = 0 -- Track currently selected petition index + self.frame = { l=0, t=0, r=0, b=0 } -- Base frame; child widgets define actual layout + + -- Define child views for this overlay + self:addviews{ + -- List showing petitioners and summary info + widgets.List{ + frame={l=60, r=28, t=30, b=14}, -- Position relative to screen edges + frame_style=gui.FRAME_INTERIOR, + view_id='list', -- Identifier for lookup via self.subviews + on_select=self:callback('onZoom'), -- Called when selection changes + }, + -- Footer panel containing extended description text + widgets.Panel{ + view_id='footer', + frame={l=6, r=28, b=3, h=10}, + frame_style=gui.FRAME_INTERIOR, + subviews={ + -- Wrapped label that shows detailed skill info for the selected unit + widgets.WrappedLabel{ + frame={l=0, h=7}, + view_id='desc', + auto_height=false, + -- Text is computed dynamically based on the current list selection + text_to_wrap=function() + local _, choice = self.subviews.list:getSelected() + return choice and choice.text_long or '' + end, + }, + }, + }, + } +end + +-- Builds or refreshes the list contents based on the currently selected petition +function PetitionersOverlay:initListChoices() + local choices = {} -- Accumulates entries for the List widget + + local agmt_id = df.global.plotinfo.petitions[self.last_selected_petition] + if not agmt_id then + --print("no id for selected petition or selected petition invalid") -- No petition selected + return + end + local agmt = df.global.world.agreements.all[agmt_id] + if not agmt then + --print("error no petition found") + return + end + + local party0 = agmt.parties[0] -- First party involved in the agreement + + -- Collect all historical figure IDs associated with this petition + local histfig_ids = {} + if #party0.histfig_ids > 0 then + -- Direct histfig references + for _, hf_id in ipairs(party0.histfig_ids) do histfig_ids[hf_id] = true end + elseif #party0.entity_ids > 0 then + -- Indirect references via an entity + local ent = df.global.world.entities.all[party0.entity_ids[0]] + if ent then + for _, hf in ipairs(ent.hist_figures) do histfig_ids[hf.id] = true end + end + end + + -- Iterate over collected historical figures and resolve them to active units + for hf_id, _ in pairs(histfig_ids) do + local u = nil + for _, unit in ipairs(df.global.world.units.active) do + if unit.hist_figure_id == hf_id then + u = unit -- Found the active unit for this histfig + break + end + end + + if u then + -- Resolve and localize unit names + local u_name = dfhack.translation.translateName(u.name) + local trans_name = dfhack.translation.translateName(u.name, true, true) + local u_caste = get_caste_name(u.race, u.caste, u.profession) + local u_race = dfhack.units.getRaceName(u) == "DWARF" and " (DWARF)" or "" + + local info_text = "" -- Long description text (skills) + + -- Collect skills with rating > 0 + if u.status.current_soul.skills then + local skills_copy = {} + for _, skill in ipairs(u.status.current_soul.skills) do + if skill.rating > 0 then + table.insert(skills_copy, skill) + end + end + + -- Sort skills by rating, highest first + table.sort(skills_copy, function(a, b) + return a.rating > b.rating + end) + + -- Append skill names and ratings into a single string + for _, skill in ipairs(skills_copy) do + info_text = info_text .. df.job_skill[skill.id] .. ": " .. skill.rating .. " " + end + end + + if info_text == "" then info_text = "None" end + + -- Short text shown in the list, long text shown in the footer + local text = u_name .. " (" .. trans_name .. ") - " .. u_caste .. u_race + local text_long = info_text + + table.insert(choices, {text=text, text_long=text_long, data={unit=u}}) + end + end + + -- Apply the built choices to the List widget + self.subviews.list:setChoices(choices) +end + +-- Checks whether the petition list size or selected petition has changed +-- and refreshes the list contents if necessary +function PetitionersOverlay:ListChangeCheck() + local current_size = #df.global.plotinfo.petitions + if current_size < 1 then + self.subviews.list:setChoices({}) + return + end + local current_selected = getActivePetitionRow(self) + if current_selected == nil then return end + if current_size ~= self.last_petitions_size or current_selected ~= self.last_selected_petition then + self.last_petitions_size = current_size + self.last_selected_petition = current_selected + --print("Petitions list or selection changed: " .. current_size) + self:initListChoices() + end +end + +-- Called every render frame while the overlay is visible +function PetitionersOverlay:onRenderFrame(dc, rect) + -- Perform one-time initialization on the first render + if self.firstrender == nil or self.firstrender then + --print("first render" ) + self:initListChoices() + self.firstrender = false + end + + -- Continuously monitor for petition list or selection changes + self:ListChangeCheck() +end + +-- Called when the user selects an entry in the list +-- Zooms the camera to the selected unit and updates layout if needed +function PetitionersOverlay:onZoom() + local _, choice = self.subviews.list:getSelected() + if not choice then return end + local unit = choice.data.unit + local target = xyz2pos(dfhack.units.getPosition(unit)) + dfhack.gui.revealInDwarfmodeMap(target, true, true) + local desc = self.subviews.desc + if desc.frame_body then desc:updateLayout() end +end + +-- Register this overlay widget so DFHack can discover and load it +OVERLAY_WIDGETS = {PetitionersOverlay=PetitionersOverlay} From 9a6c7aa97c0ca92ff3d4a4340d790d198ff85a6c Mon Sep 17 00:00:00 2001 From: woutersparre <63191859+woutersparre@users.noreply.github.com> Date: Thu, 18 Dec 2025 20:28:08 +0100 Subject: [PATCH 2/3] Add files via upload --- petitioners.lua | 183 ++++++++++++++---------------------------------- 1 file changed, 54 insertions(+), 129 deletions(-) diff --git a/petitioners.lua b/petitioners.lua index 18ba22132..6f2f1ee0e 100644 --- a/petitioners.lua +++ b/petitioners.lua @@ -5,49 +5,6 @@ local gui = require('gui') -- GUI helpers (pens, frames, layout) local overlay = require('plugins.overlay') -- Overlay framework (OverlayWidget base) local widgets = require('gui.widgets') -- Standard DFHack UI widgets (List, Panel, Label, etc.) -local utils = require('utils') -- Misc DFHack utilities - --- ------------------- --- Utility functions --- ------------------- - --- Determines which petition row is currently selected in the vanilla UI --- by comparing screen texture positions. This is a heuristic that tracks --- which row's screen texpos matches the initially captured value. -function getActivePetitionRow(self) - local starty = 6 -- Y offset where the first petition row starts - local steps = 3 -- Vertical spacing between petition rows - local listlength = #df.global.plotinfo.petitions -- Number of petitions currently listed - local gps = df.global.gps -- Global screen state (texture positions, dimensions) - if not gps then - return nil -- If GPS is unavailable, we cannot determine selection - end - - -- Capture the initially selected texpos once. This serves as the reference - -- value that identifies the currently highlighted row. - if self.tocheck == nil or self.tocheck == 0 then - self.tocheck = gps.screentexpos_lower[6 * gps.dimy + starty] or 0 - --print("tocheck " .. self.tocheck) - end - - -- Iterate over all visible petition rows and compare their texpos - -- against the captured reference. The matching row index is returned. - for i = 0, listlength - 1 do - local y = starty + i * steps - local idx = (6 * gps.dimy) + y - local tex = gps.screentexpos_lower[idx] or 0 - - if tex == self.tocheck then - return i -- Found the active petition row - end - end - return nil -end - --- Helper that returns a localized caste/profession name for a unit -local function get_caste_name(race, caste, profession) - return dfhack.units.getCasteProfessionName(race, caste, profession) -end -- ------------------- -- PetitionersOverlay @@ -66,36 +23,35 @@ PetitionersOverlay.ATTRS{ -- Initialization runs once when the widget is created function PetitionersOverlay:init() self.firstrender = true -- Flag to perform one-time setup on first render - self.tocheck = nil -- Stored texpos reference for row detection self.last_petitions_size = #df.global.plotinfo.petitions -- Track petition count - self.last_selected_petition = 0 -- Track currently selected petition index self.frame = { l=0, t=0, r=0, b=0 } -- Base frame; child widgets define actual layout -- Define child views for this overlay self:addviews{ -- List showing petitioners and summary info - widgets.List{ - frame={l=60, r=28, t=30, b=14}, -- Position relative to screen edges + widgets.Panel{ + frame={l=6, r=28, b=3, h=10}, frame_style=gui.FRAME_INTERIOR, - view_id='list', -- Identifier for lookup via self.subviews - on_select=self:callback('onZoom'), -- Called when selection changes + frame_background=gui.CLEAR_PEN, + subviews={ + widgets.List{ + view_id='list', -- Identifier for lookup via self.subviews + on_select=self:callback('onSelect'), -- Called when selection changes + }, + }, }, - -- Footer panel containing extended description text + + -- Info panel containing extended description text widgets.Panel{ - view_id='footer', - frame={l=6, r=28, b=3, h=10}, + frame={l=60, r=28, t=30, b=14}, frame_style=gui.FRAME_INTERIOR, subviews={ -- Wrapped label that shows detailed skill info for the selected unit - widgets.WrappedLabel{ - frame={l=0, h=7}, + widgets.Label{ view_id='desc', auto_height=false, -- Text is computed dynamically based on the current list selection - text_to_wrap=function() - local _, choice = self.subviews.list:getSelected() - return choice and choice.text_long or '' - end, + text='', }, }, }, @@ -103,79 +59,71 @@ function PetitionersOverlay:init() end -- Builds or refreshes the list contents based on the currently selected petition -function PetitionersOverlay:initListChoices() - local choices = {} -- Accumulates entries for the List widget - - local agmt_id = df.global.plotinfo.petitions[self.last_selected_petition] - if not agmt_id then - --print("no id for selected petition or selected petition invalid") -- No petition selected +function PetitionersOverlay:SetListChoices() + if #df.global.plotinfo.petitions == 0 then + self.subviews.list:setChoices({}) + self.subviews.desc:setText("") + self.subviews.desc:updateLayout() return end + + local agmt_id = df.global.game.main_interface.petitions.selected_agreement_id local agmt = df.global.world.agreements.all[agmt_id] - if not agmt then - --print("error no petition found") - return - end - - local party0 = agmt.parties[0] -- First party involved in the agreement + local party = agmt.parties[0] -- First party involved in the agreement -- Collect all historical figure IDs associated with this petition local histfig_ids = {} - if #party0.histfig_ids > 0 then + if #party.histfig_ids > 0 then -- Direct histfig references - for _, hf_id in ipairs(party0.histfig_ids) do histfig_ids[hf_id] = true end - elseif #party0.entity_ids > 0 then + for _, hf_id in ipairs(party.histfig_ids) do histfig_ids[hf_id] = true end + elseif #party.entity_ids > 0 then -- Indirect references via an entity - local ent = df.global.world.entities.all[party0.entity_ids[0]] + local ent = df.historical_entity.find(party.entity_ids[0]) if ent then for _, hf in ipairs(ent.hist_figures) do histfig_ids[hf.id] = true end end end + + + local choices = {} -- Accumulates entries for the List widget -- Iterate over collected historical figures and resolve them to active units for hf_id, _ in pairs(histfig_ids) do - local u = nil - for _, unit in ipairs(df.global.world.units.active) do - if unit.hist_figure_id == hf_id then - u = unit -- Found the active unit for this histfig - break - end - end + local hf = df.historical_figure.find(hf_id) + local u = (hf) and df.unit.find(hf.unit_id) or nil if u then -- Resolve and localize unit names local u_name = dfhack.translation.translateName(u.name) local trans_name = dfhack.translation.translateName(u.name, true, true) - local u_caste = get_caste_name(u.race, u.caste, u.profession) - local u_race = dfhack.units.getRaceName(u) == "DWARF" and " (DWARF)" or "" + local u_caste = dfhack.units.getCasteProfessionName(u.race, u.caste, u.profession) + + local lines = {} - local info_text = "" -- Long description text (skills) - - -- Collect skills with rating > 0 if u.status.current_soul.skills then local skills_copy = {} + for _, skill in ipairs(u.status.current_soul.skills) do if skill.rating > 0 then table.insert(skills_copy, skill) end end - -- Sort skills by rating, highest first - table.sort(skills_copy, function(a, b) - return a.rating > b.rating - end) + table.sort(skills_copy, function(a, b)return a.rating > b.rating end) --sort skills high to low - -- Append skill names and ratings into a single string for _, skill in ipairs(skills_copy) do - info_text = info_text .. df.job_skill[skill.id] .. ": " .. skill.rating .. " " + --local skill_name_pen = COLOR_CYAN -- for skill name + --local skill_rating_pen = COLOR_WHITE -- for rating + + table.insert(lines, { text = string.format("%-26s", df.job_skill[skill.id]), pen = COLOR_RED }) + table.insert(lines, { text = string.format(" %2d", skill.rating), pen = COLOR_BLUE }) + table.insert(lines, NEWLINE) end end - if info_text == "" then info_text = "None" end - - -- Short text shown in the list, long text shown in the footer - local text = u_name .. " (" .. trans_name .. ") - " .. u_caste .. u_race - local text_long = info_text + -- Short text shown in the list, long text shown in the info panel + local text = u_name .. " (" .. trans_name .. ") - " .. u_caste + local text_long = (#lines > 0) and lines or '' table.insert(choices, {text=text, text_long=text_long, data={unit=u}}) end @@ -185,47 +133,24 @@ function PetitionersOverlay:initListChoices() self.subviews.list:setChoices(choices) end --- Checks whether the petition list size or selected petition has changed --- and refreshes the list contents if necessary -function PetitionersOverlay:ListChangeCheck() - local current_size = #df.global.plotinfo.petitions - if current_size < 1 then - self.subviews.list:setChoices({}) - return - end - local current_selected = getActivePetitionRow(self) - if current_selected == nil then return end - if current_size ~= self.last_petitions_size or current_selected ~= self.last_selected_petition then - self.last_petitions_size = current_size - self.last_selected_petition = current_selected - --print("Petitions list or selection changed: " .. current_size) - self:initListChoices() - end -end - -- Called every render frame while the overlay is visible function PetitionersOverlay:onRenderFrame(dc, rect) - -- Perform one-time initialization on the first render - if self.firstrender == nil or self.firstrender then - --print("first render" ) - self:initListChoices() - self.firstrender = false - end - - -- Continuously monitor for petition list or selection changes - self:ListChangeCheck() + if not df.global.game.main_interface.petitions.open then return end + self:SetListChoices() end --- Called when the user selects an entry in the list --- Zooms the camera to the selected unit and updates layout if needed -function PetitionersOverlay:onZoom() +-- Called on selection of an entry in the list +-- Updates description text and zooms the camera to the selected unit +function PetitionersOverlay:onSelect() local _, choice = self.subviews.list:getSelected() if not choice then return end + + self.subviews.desc:setText(choice.text_long or '') + self.subviews.desc:updateLayout() + local unit = choice.data.unit local target = xyz2pos(dfhack.units.getPosition(unit)) dfhack.gui.revealInDwarfmodeMap(target, true, true) - local desc = self.subviews.desc - if desc.frame_body then desc:updateLayout() end end -- Register this overlay widget so DFHack can discover and load it From 227c339f5719f8cc47454460d915d66de995c7f4 Mon Sep 17 00:00:00 2001 From: woutersparre <63191859+woutersparre@users.noreply.github.com> Date: Thu, 18 Dec 2025 22:00:08 +0100 Subject: [PATCH 3/3] Add files via upload --- petitioners.lua | 55 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 53 insertions(+), 2 deletions(-) diff --git a/petitioners.lua b/petitioners.lua index 6f2f1ee0e..a296730c7 100644 --- a/petitioners.lua +++ b/petitioners.lua @@ -57,6 +57,57 @@ function PetitionersOverlay:init() }, } end +local skill_color_ranges = { + {0, 0, COLOR_GREY}, -- MINING + {1, 2, COLOR_BROWN}, -- Woodcutting / Carpentry + {3, 4, COLOR_BROWN}, -- Engrave / Masonry + {5, 23, COLOR_YELLOW}, -- Cooking / artisan crafting + {24, 28, COLOR_ORANGE}, -- Smelt / forge + {29, 30, COLOR_CYAN}, -- Gem cutting / encrusting + {31, 36, COLOR_BROWN}, -- Crafting + {37, 53, COLOR_RED}, -- Weapon skills + {54, 56, COLOR_MAGENTA}, -- Mechanics, magic, sneak + {57, 62, COLOR_LIME}, -- Medical skills (healing green) + {63, 69, COLOR_TAN}, -- Odd jobs + {70, 82, COLOR_BLUE}, -- Social skills + {83, 87, COLOR_PURPLE}, -- Inner skills like discipline + {88, 91, COLOR_MAGENTA}, -- Writing / Poetry / Reading + {92, 96, COLOR_BLUE}, -- Teaching / Leadership + {97, 106, COLOR_DARKRED}, -- Military skills + {107, 113, COLOR_BROWN}, -- Artisan odd jobs + {114, 115, COLOR_GREEN}, -- Climbing / Gelding + {116, 122, COLOR_MAGENTA}, -- Artistic skills + {123, 130, COLOR_TEAL}, -- Thinking / Engineering + {131, 132, COLOR_MAGENTA}, -- Papermaking / bookbinding + {133, 134, COLOR_BROWN}, -- Cut / carve stone +} + + +local function get_skill_color(skill_id) + if not skill_id then return COLOR_GREY end + for _, range in ipairs(skill_color_ranges) do + local min_id, max_id, color = table.unpack(range) + if skill_id >= min_id and skill_id <= max_id then + return color + end + end + return COLOR_GREY -- fallback +end + +local function get_rating_color(rating) + if not rating then return COLOR_GREY end + if rating <= 4 then + return COLOR_GREY + elseif rating <= 8 then + return COLOR_WHITE + elseif rating <= 12 then + return COLOR_YELLOW + elseif rating <= 16 then + return COLOR_ORANGE + else -- 17-20 + return COLOR_RED + end +end -- Builds or refreshes the list contents based on the currently selected petition function PetitionersOverlay:SetListChoices() @@ -115,8 +166,8 @@ function PetitionersOverlay:SetListChoices() --local skill_name_pen = COLOR_CYAN -- for skill name --local skill_rating_pen = COLOR_WHITE -- for rating - table.insert(lines, { text = string.format("%-26s", df.job_skill[skill.id]), pen = COLOR_RED }) - table.insert(lines, { text = string.format(" %2d", skill.rating), pen = COLOR_BLUE }) + table.insert(lines, { text = string.format("%-26s", df.job_skill[skill.id]), pen = get_skill_color(skill.id) }) + table.insert(lines, { text = string.format("%2d", skill.rating), pen = get_rating_color(skill.rating) }) table.insert(lines, NEWLINE) end end