From 5e7d81c2bc8baa650c45f9554003605b6f75b6e6 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 12 Mar 2023 13:58:03 +0100 Subject: [PATCH 01/61] add biomes.lua --- gui/biomes.lua | 213 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 gui/biomes.lua diff --git a/gui/biomes.lua b/gui/biomes.lua new file mode 100644 index 000000000..2199a2d61 --- /dev/null +++ b/gui/biomes.lua @@ -0,0 +1,213 @@ +-- Show biomes on the map. + +local gui = require('gui') +local widgets = require('gui.widgets') +local guidm = require('gui.dwarfmode') + +local biomeTypeNames = { + MOUNTAIN = "Mountain", + GLACIER = "Glacier", + TUNDRA = "Tundra", + SWAMP_TEMPERATE_FRESHWATER = "Temperate Freshwater Swamp", + SWAMP_TEMPERATE_SALTWATER = "Temperate Saltwater Swamp", + MARSH_TEMPERATE_FRESHWATER = "Temperate Freshwater Marsh", + MARSH_TEMPERATE_SALTWATER = "Temperate Saltwater Marsh", + SWAMP_TROPICAL_FRESHWATER = "Tropical Freshwater Swamp", + SWAMP_TROPICAL_SALTWATER = "Tropical Saltwater Swamp", + SWAMP_MANGROVE = "Mangrove Swamp", + MARSH_TROPICAL_FRESHWATER = "Tropical Freshwater Marsh", + MARSH_TROPICAL_SALTWATER = "Tropical Saltwater Marsh", + FOREST_TAIGA = "Taiga Forest", + FOREST_TEMPERATE_CONIFER = "Temperate Conifer Forest", + FOREST_TEMPERATE_BROADLEAF = "Temperate Broadleaf Forest", + FOREST_TROPICAL_CONIFER = "Tropical Conifer Forest", + FOREST_TROPICAL_DRY_BROADLEAF = "Tropical Dry Broadleaf Forest", + FOREST_TROPICAL_MOIST_BROADLEAF = "Tropical Moist Broadleaf Forest", + GRASSLAND_TEMPERATE = "Temperate Grassland", + SAVANNA_TEMPERATE = "Temperate Savanna", + SHRUBLAND_TEMPERATE = "Temperate Shrubland", + GRASSLAND_TROPICAL = "Tropical Grassland", + SAVANNA_TROPICAL = "Tropical Savanna", + SHRUBLAND_TROPICAL = "Tropical Shrubland", + DESERT_BADLAND = "Badland Desert", + DESERT_ROCK = "Rock Desert", + DESERT_SAND = "Sand Desert", + OCEAN_TROPICAL = "Tropical Ocean", + OCEAN_TEMPERATE = "Temperate Ocean", + OCEAN_ARCTIC = "Arctic Ocean", + POOL_TEMPERATE_FRESHWATER = "Temperate Freshwater Pool", + POOL_TEMPERATE_BRACKISHWATER = "Temperate Brackishwater Pool", + POOL_TEMPERATE_SALTWATER = "Temperate Saltwater Pool", + POOL_TROPICAL_FRESHWATER = "Tropical Freshwater Pool", + POOL_TROPICAL_BRACKISHWATER = "Tropical Brackishwater Pool", + POOL_TROPICAL_SALTWATER = "Tropical Saltwater Pool", + LAKE_TEMPERATE_FRESHWATER = "Temperate Freshwater Lake", + LAKE_TEMPERATE_BRACKISHWATER = "Temperate Brackishwater Lake", + LAKE_TEMPERATE_SALTWATER = "Temperate Saltwater Lake", + LAKE_TROPICAL_FRESHWATER = "Tropical Freshwater Lake", + LAKE_TROPICAL_BRACKISHWATER = "Tropical Brackishwater Lake", + LAKE_TROPICAL_SALTWATER = "Tropical Saltwater Lake", + RIVER_TEMPERATE_FRESHWATER = "Temperate Freshwater River", + RIVER_TEMPERATE_BRACKISHWATER = "Temperate Brackishwater River", + RIVER_TEMPERATE_SALTWATER = "Temperate Saltwater River", + RIVER_TROPICAL_FRESHWATER = "Tropical Freshwater River", + RIVER_TROPICAL_BRACKISHWATER = "Tropical Brackishwater River", + RIVER_TROPICAL_SALTWATER = "Tropical Saltwater River", + SUBTERRANEAN_WATER = "Subterranean Water", + SUBTERRANEAN_CHASM = "Subterranean Chasm", + SUBTERRANEAN_LAVA = "Subterranean Lava", +} + +local function find(t, predicate) + for k, item in pairs(t) do + if predicate(k, item) then + return k, item + end + end + return nil +end + +local biomesMap = {} +local biomeList = {} +local function gatherBiomeInfo() + local maxX, maxY, maxZ = dfhack.maps.getTileSize() + maxX = maxX - 1; maxY = maxY - 1; maxZ = maxZ - 1 + + local z = df.global.window_z + + for y = 0, maxY do + for x = 0, maxX do + local rgnX, rgnY = dfhack.maps.getTileBiomeRgn(x,y,z) + local biomeTypeId = dfhack.maps.getBiomeType(rgnX, rgnY) + local biome = dfhack.maps.getRegionBiome(rgnX, rgnY) + + local biomesZ = biomesMap[z] + if not biomesZ then + biomesZ = {} + biomesMap[z] = biomesZ + end + local biomesZY = biomesZ[y] + if not biomesZY then + biomesZY = {} + biomesZ[y] = biomesZY + end + + local function currentBiome(_, item) + return item.biome == biome + end + local ix = find(biomeList, currentBiome) + if not ix then + local ch = string.char(string.byte('a') + #biomeList) + table.insert(biomeList, {biome = biome, char = ch, typeId = biomeTypeId}) + ix = #biomeList + end + + biomesZY[x] = ix + end + end +end + +gatherBiomeInfo() + +local TITLE = "Biomes" + +BiomeVisualizerLegend = nil -- for debugging purposes +BiomeVisualizerLegend = defclass(BiomeVisualizerLegend, widgets.Window) +BiomeVisualizerLegend.ATTRS { + frame_title=TITLE, + frame_inset=0, + resizable=true, + autoarrange_subviews = true, + autoarrange_gap = 1, + frame = { + w = 47, + h = 10, + l = 5, + t = 8, + }, +} + +local function GetBiomeName(biome, biomeTypeId) + -- based on probe.cpp + local sav = biome.savagery + local evi = biome.evilness; + local sindex = sav > 65 and 2 or sav < 33 and 0 or 1 + local eindex = evi > 65 and 2 or evi < 33 and 0 or 1 + local surr = sindex + eindex * 3 +1; --in Lua arrays are 1-based + + local surroundings = { + "Serene", "Mirthful", "Joyous Wilds", + "Calm", "Wilderness", "Untamed Wilds", + "Sinister", "Haunted", "Terrifying" + } + local surrounding = surroundings[surr] + + local name = biomeTypeNames[df.biome_type[biomeTypeId]] or "DFHACK_Unknown" + + return ([[%s %s]]):format(surrounding, name) +end + +function BiomeVisualizerLegend:init() + self:addviews{ + widgets.List{ + view_id = 'list', + frame = { t = 0 }, + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, + --cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + --on_select = self:callback('onSelectEntry'), + }, + } + + local choices = {} + for _, biomeExt in ipairs(biomeList) do + table.insert(choices, { + text = ([[%s: %s]]):format(biomeExt.char, GetBiomeName(biomeExt.biome, biomeExt.typeId)), + biomeTypeId = biomeExt.typeId, + }) + end + self.subviews.list:setChoices(choices) +end + +BiomeVisualizer = defclass(BiomeVisualizer, gui.ZScreen) +BiomeVisualizer.ATTRS{ + focus_path='BiomeVisualizer', +} + +local function getMapViewBounds() + local dims = dfhack.gui.getDwarfmodeViewDims() + local x = df.global.window_x + local y = df.global.window_y + return {x1 = dims.map_x1 + x, + y1 = dims.map_y1 + y, + x2 = dims.map_x2 + x, + y2 = dims.map_y2 + y, + } +end + +function BiomeVisualizer:init() + self:addviews{BiomeVisualizerLegend{}} +end + +function BiomeVisualizer:onRenderFrame(dc, rect) + BiomeVisualizer.super.onRenderFrame() + + local function get_overlay_pen(pos) + local safe_index = safe_index + -- 304 = `0`, 353 = `a` + local idxBaseTile = 353 + local biomes = biomesMap + + local N = safe_index(biomes, pos.z, pos.y, pos.x) + if not N then return end + return COLOR_RED, tostring(N), idxBaseTile + (N - 1) + end + local overlay_bounds = getMapViewBounds() + + guidm.renderMapOverlay(get_overlay_pen, overlay_bounds) +end + +function BiomeVisualizer:onDismiss() + view = nil +end + +view = view and view:raise() or BiomeVisualizer{}:show() From 4464035217138079cdc3b62ce3dee5878a734cd5 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 6 May 2023 13:50:21 +0200 Subject: [PATCH 02/61] position legend widget under minimap Few other minor changes --- gui/biomes.lua | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index 2199a2d61..be88a04d7 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -117,13 +117,14 @@ BiomeVisualizerLegend.ATTRS { frame_title=TITLE, frame_inset=0, resizable=true, + resize_min={h=5, w = 14}, autoarrange_subviews = true, autoarrange_gap = 1, frame = { w = 47, h = 10, - l = 5, - t = 8, + r = 2, + t = 18, }, } @@ -189,7 +190,7 @@ function BiomeVisualizer:init() end function BiomeVisualizer:onRenderFrame(dc, rect) - BiomeVisualizer.super.onRenderFrame() + BiomeVisualizer.super.onRenderFrame(dc, rect) local function get_overlay_pen(pos) local safe_index = safe_index @@ -201,9 +202,8 @@ function BiomeVisualizer:onRenderFrame(dc, rect) if not N then return end return COLOR_RED, tostring(N), idxBaseTile + (N - 1) end - local overlay_bounds = getMapViewBounds() - guidm.renderMapOverlay(get_overlay_pen, overlay_bounds) + guidm.renderMapOverlay(get_overlay_pen, nil) -- nil for bounds means entire viewport end function BiomeVisualizer:onDismiss() From 6c451f7c4447dc97edeed301c0c960272e5c164c Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 6 May 2023 17:38:45 +0200 Subject: [PATCH 03/61] update biome info when changing z-levels Other minor tweaks and an attempt at slightly improving performance via caching biome region info --- gui/biomes.lua | 63 ++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 54 insertions(+), 9 deletions(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index be88a04d7..c6c397d6a 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -67,19 +67,37 @@ local function find(t, predicate) return nil end +local regionBiomeMap = {} local biomesMap = {} local biomeList = {} -local function gatherBiomeInfo() +local function gatherBiomeInfo(z) local maxX, maxY, maxZ = dfhack.maps.getTileSize() maxX = maxX - 1; maxY = maxY - 1; maxZ = maxZ - 1 - local z = df.global.window_z + local z = z or df.global.window_z + --for z = 0, maxZ do for y = 0, maxY do for x = 0, maxX do local rgnX, rgnY = dfhack.maps.getTileBiomeRgn(x,y,z) - local biomeTypeId = dfhack.maps.getBiomeType(rgnX, rgnY) - local biome = dfhack.maps.getRegionBiome(rgnX, rgnY) + if rgnX == nil then goto continue end + + local regionBiomesX = regionBiomeMap[rgnX] + if not regionBiomesX then + regionBiomesX = {} + regionBiomeMap[rgnX] = regionBiomesX + end + local regionBiomesXY = regionBiomesX[rgnY] + if not regionBiomesXY then + regionBiomesXY = { + biomeTypeId = dfhack.maps.getBiomeType(rgnX, rgnY), + biome = dfhack.maps.getRegionBiome(rgnX, rgnY), + } + regionBiomesX[rgnY] = regionBiomesXY + end + + local biomeTypeId = regionBiomesXY.biomeTypeId + local biome = regionBiomesXY.biome local biomesZ = biomesMap[z] if not biomesZ then @@ -103,8 +121,11 @@ local function gatherBiomeInfo() end biomesZY[x] = ix + + ::continue:: end end + --end end gatherBiomeInfo() @@ -155,12 +176,21 @@ function BiomeVisualizerLegend:init() frame = { t = 0 }, text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, --cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, - --on_select = self:callback('onSelectEntry'), + on_select = self:callback('onSelectEntry'), }, } - local choices = {} - for _, biomeExt in ipairs(biomeList) do + self:UpdateChoices() +end + +function BiomeVisualizerLegend:onSelectEntry(idx, option) + self.SelectedIndex = idx +end + +function BiomeVisualizerLegend:UpdateChoices() + local choices = self.subviews.list:getChoices() or {} + for i = #choices + 1, #biomeList do + local biomeExt = biomeList[i] table.insert(choices, { text = ([[%s: %s]]):format(biomeExt.char, GetBiomeName(biomeExt.biome, biomeExt.typeId)), biomeTypeId = biomeExt.typeId, @@ -186,21 +216,32 @@ local function getMapViewBounds() end function BiomeVisualizer:init() - self:addviews{BiomeVisualizerLegend{}} + self:addviews{BiomeVisualizerLegend{view_id = 'legend'}} end function BiomeVisualizer:onRenderFrame(dc, rect) BiomeVisualizer.super.onRenderFrame(dc, rect) + local z = df.global.window_z + if not biomesMap[z] then + gatherBiomeInfo(z) + self.subviews.legend:UpdateChoices() + end + local function get_overlay_pen(pos) + local self = self local safe_index = safe_index -- 304 = `0`, 353 = `a` local idxBaseTile = 353 + -- 2586 = yellow-ish indicator + local idxHighlightedTile = 2585 local biomes = biomesMap local N = safe_index(biomes, pos.z, pos.y, pos.x) if not N then return end - return COLOR_RED, tostring(N), idxBaseTile + (N - 1) + + local idxTile = (N == self.subviews.legend.SelectedIndex) and idxHighlightedTile or idxBaseTile + (N - 1) + return COLOR_RED, tostring(N), idxTile end guidm.renderMapOverlay(get_overlay_pen, nil) -- nil for bounds means entire viewport @@ -210,4 +251,8 @@ function BiomeVisualizer:onDismiss() view = nil end +if not dfhack.isMapLoaded() then + qerror('gui/biomes requires a map to be loaded') +end + view = view and view:raise() or BiomeVisualizer{}:show() From 689cc5cf54b533b6003b1d4af73967a1578cb4f4 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 6 May 2023 21:38:01 +0200 Subject: [PATCH 04/61] add "tooltip" --- gui/biomes.lua | 138 ++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 131 insertions(+), 7 deletions(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index c6c397d6a..f260542cf 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -1,5 +1,7 @@ -- Show biomes on the map. +local RELOAD = false -- set to true to help with debugging + local gui = require('gui') local widgets = require('gui.widgets') local guidm = require('gui.dwarfmode') @@ -130,17 +132,17 @@ end gatherBiomeInfo() +-------------------------------------------------------------------------------- + local TITLE = "Biomes" -BiomeVisualizerLegend = nil -- for debugging purposes +if RELOAD then BiomeVisualizerLegend = nil end BiomeVisualizerLegend = defclass(BiomeVisualizerLegend, widgets.Window) BiomeVisualizerLegend.ATTRS { frame_title=TITLE, frame_inset=0, resizable=true, resize_min={h=5, w = 14}, - autoarrange_subviews = true, - autoarrange_gap = 1, frame = { w = 47, h = 10, @@ -174,8 +176,7 @@ function BiomeVisualizerLegend:init() widgets.List{ view_id = 'list', frame = { t = 0 }, - text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, - --cursor_pen = { fg = COLOR_BLACK, bg = COLOR_GREEN }, + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, -- this makes selection stand out more on_select = self:callback('onSelectEntry'), }, } @@ -194,11 +195,128 @@ function BiomeVisualizerLegend:UpdateChoices() table.insert(choices, { text = ([[%s: %s]]):format(biomeExt.char, GetBiomeName(biomeExt.biome, biomeExt.typeId)), biomeTypeId = biomeExt.typeId, + biome = biomeExt.biome, }) end self.subviews.list:setChoices(choices) end +do -- implementation of onMouseHoverEntry(idx, option) + function BiomeVisualizerLegend:onRenderFrame(dc, rect) + BiomeVisualizerLegend.super.onRenderFrame(self, dc, rect) + + local list = self.subviews.list + local currentHoverIx = list:getIdxUnderMouse() + local oldIx = self.HoverIndex + if currentHoverIx ~= oldIx then + self.HoverIndex = currentHoverIx + if self.onMouseHoverEntry then + local choices = list:getChoices() + self:onMouseHoverEntry(currentHoverIx, choices[currentHoverIx]) + end + end + end +end + +local function add_field_text(lines, biome, field_name) + lines[#lines+1] = ("%s: %s"):format(field_name, biome[field_name]) + lines[#lines+1] = NEWLINE +end + +function BiomeVisualizerLegend:onMouseHoverEntry(idx, option) + if not idx then + self:ShowTooltip(nil) + return + end + + local text = {} + text[#text+1] = ("type: %s"):format(df.biome_type[option.biomeTypeId]) + text[#text+1] = NEWLINE + + local biome = option.biome + + add_field_text(text, biome, "savagery") + add_field_text(text, biome, "evilness") + table.insert(text, NEWLINE) + + add_field_text(text, biome, "elevation") + add_field_text(text, biome, "rainfall") + add_field_text(text, biome, "drainage") + add_field_text(text, biome, "vegetation") + add_field_text(text, biome, "temperature") + add_field_text(text, biome, "volcanism") + table.insert(text, NEWLINE) + + local flags = biome.flags + if flags.is_lake then + text[#text+1] = "lake" + text[#text+1] = NEWLINE + end + if flags.is_brook then + text[#text+1] = "brook" + text[#text+1] = NEWLINE + end + + self:ShowTooltip(text) +end + +function BiomeVisualizerLegend:ShowTooltip(text) + self.TooltipWindow = self.TooltipWindow or self.parent_view.subviews.legend_tooltip + self.TooltipWindow:ShowTooltip(text) +end + +-------------------------------------------------------------------------------- + +if RELOAD then TooltipWindow = nil end +TooltipWindow = defclass(TooltipWindow, widgets.Window) + +local function calc_tooltip_frame(frame) + local frame = copyall(frame) + frame.t = frame.t + frame.h + frame.h = 15 + return frame +end + +TooltipWindow.ATTRS { + visible = false, + frame_title=DEFAULT_NIL, + frame_inset=0, + resizable=false, + frame_style = gui.PANEL_FRAME, + autoarrange_subviews = true, + autoarrange_gap = 0, + frame = calc_tooltip_frame(BiomeVisualizerLegend.ATTRS.frame), + -- + parent_window = DEFAULT_NIL, +} + +function TooltipWindow:init() + self:addviews{ + widgets.Label{ + view_id = 'label', + frame = {t = 0, h = 1000}, + auto_height = false, + --wtf??? without this the label is always a single line + text={NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,} + }, + } +end + +function TooltipWindow:ShowTooltip(text) + if not text or text == "" or not self.parent_window then + self.visible = false + return + end + + local parent = self.parent_window + local lbl = self.subviews.label + lbl:setText(text) + self.visible = true +end + +-------------------------------------------------------------------------------- + +if RELOAD then BiomeVisualizer = nil end BiomeVisualizer = defclass(BiomeVisualizer, gui.ZScreen) BiomeVisualizer.ATTRS{ focus_path='BiomeVisualizer', @@ -216,11 +334,13 @@ local function getMapViewBounds() end function BiomeVisualizer:init() - self:addviews{BiomeVisualizerLegend{view_id = 'legend'}} + local legend = BiomeVisualizerLegend{view_id = 'legend'} + local legend_tooltip = TooltipWindow{view_id = 'legend_tooltip', parent_window = legend} + self:addviews{legend, legend_tooltip} end function BiomeVisualizer:onRenderFrame(dc, rect) - BiomeVisualizer.super.onRenderFrame(dc, rect) + BiomeVisualizer.super.onRenderFrame(self, dc, rect) local z = df.global.window_z if not biomesMap[z] then @@ -255,4 +375,8 @@ if not dfhack.isMapLoaded() then qerror('gui/biomes requires a map to be loaded') end +if RELOAD and view then + view:dismiss() +end + view = view and view:raise() or BiomeVisualizer{}:show() From 012eff858fd71fbb15ed02670ef7df04268b3864 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 7 May 2023 15:31:38 +0200 Subject: [PATCH 05/61] add selected icon, improve text mode experience --- gui/biomes.lua | 44 +++++++++++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 17 deletions(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index f260542cf..a10b73ec1 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -6,6 +6,9 @@ local gui = require('gui') local widgets = require('gui.widgets') local guidm = require('gui.dwarfmode') +local TILE_HIGHLIGHTED = 2585 -- yellow-ish indicator +local TILE_STARTING_SYMBOL = 353 -- `a` + local biomeTypeNames = { MOUNTAIN = "Mountain", GLACIER = "Glacier", @@ -176,6 +179,7 @@ function BiomeVisualizerLegend:init() widgets.List{ view_id = 'list', frame = { t = 0 }, + icon_width = 1, text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, -- this makes selection stand out more on_select = self:callback('onSelectEntry'), }, @@ -184,6 +188,19 @@ function BiomeVisualizerLegend:init() self:UpdateChoices() end +local PEN_ACTIVE_ICON = dfhack.pen.parse{tile=TILE_HIGHLIGHTED} +local PEN_NO_ICON = nil + +function BiomeVisualizerLegend:get_icon_pen_callback(ix) + return function () + if self.SelectedIndex == ix then + return PEN_ACTIVE_ICON + else + return PEN_NO_ICON + end + end +end + function BiomeVisualizerLegend:onSelectEntry(idx, option) self.SelectedIndex = idx end @@ -194,6 +211,7 @@ function BiomeVisualizerLegend:UpdateChoices() local biomeExt = biomeList[i] table.insert(choices, { text = ([[%s: %s]]):format(biomeExt.char, GetBiomeName(biomeExt.biome, biomeExt.typeId)), + icon = self:get_icon_pen_callback(#choices+1), biomeTypeId = biomeExt.typeId, biome = biomeExt.biome, }) @@ -322,17 +340,6 @@ BiomeVisualizer.ATTRS{ focus_path='BiomeVisualizer', } -local function getMapViewBounds() - local dims = dfhack.gui.getDwarfmodeViewDims() - local x = df.global.window_x - local y = df.global.window_y - return {x1 = dims.map_x1 + x, - y1 = dims.map_y1 + y, - x2 = dims.map_x2 + x, - y2 = dims.map_y2 + y, - } -end - function BiomeVisualizer:init() local legend = BiomeVisualizerLegend{view_id = 'legend'} local legend_tooltip = TooltipWindow{view_id = 'legend_tooltip', parent_window = legend} @@ -351,17 +358,20 @@ function BiomeVisualizer:onRenderFrame(dc, rect) local function get_overlay_pen(pos) local self = self local safe_index = safe_index - -- 304 = `0`, 353 = `a` - local idxBaseTile = 353 - -- 2586 = yellow-ish indicator - local idxHighlightedTile = 2585 local biomes = biomesMap local N = safe_index(biomes, pos.z, pos.y, pos.x) if not N then return end - local idxTile = (N == self.subviews.legend.SelectedIndex) and idxHighlightedTile or idxBaseTile + (N - 1) - return COLOR_RED, tostring(N), idxTile + local idxSelected = self.subviews.legend.SelectedIndex + local idxTile = (N == idxSelected) + and TILE_HIGHLIGHTED + or TILE_STARTING_SYMBOL + (N-1) + local color = (N == idxSelected) + and COLOR_CYAN + or COLOR_GREY + local ch = string.char(string.byte('a') + (N-1)) + return color, ch, idxTile end guidm.renderMapOverlay(get_overlay_pen, nil) -- nil for bounds means entire viewport From 1602ee82e682a5bb3d8f25ecf7e79b8f390dbdfb Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 7 May 2023 15:42:04 +0200 Subject: [PATCH 06/61] remove whitespaces --- gui/biomes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index a10b73ec1..bd252b59d 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -86,7 +86,7 @@ local function gatherBiomeInfo(z) for x = 0, maxX do local rgnX, rgnY = dfhack.maps.getTileBiomeRgn(x,y,z) if rgnX == nil then goto continue end - + local regionBiomesX = regionBiomeMap[rgnX] if not regionBiomesX then regionBiomesX = {} From 44bf199f85606060859b8cef4b22781c512bf8b6 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 7 May 2023 15:43:40 +0200 Subject: [PATCH 07/61] Create biomes.rst --- doc/gui/biomes.rst | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 doc/gui/biomes.rst diff --git a/doc/gui/biomes.rst b/doc/gui/biomes.rst new file mode 100644 index 000000000..89a800bfb --- /dev/null +++ b/doc/gui/biomes.rst @@ -0,0 +1,17 @@ +gui/biomes +========== + +.. dfhack-tool:: + :summary: Shows biomes map overlay. + :tags: fort inspection map + +This script shows biomes map overlay. +Hover over a biome entry in the list to get detailed info about it. + + +Usage +----- + +:: + + gui/biomes From fc4ea454d80c48498e934f8b56455408654596dd Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 7 May 2023 15:49:41 +0200 Subject: [PATCH 08/61] move biomes.rst to the correct folder --- {doc => docs}/gui/biomes.rst | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) rename {doc => docs}/gui/biomes.rst (93%) diff --git a/doc/gui/biomes.rst b/docs/gui/biomes.rst similarity index 93% rename from doc/gui/biomes.rst rename to docs/gui/biomes.rst index 89a800bfb..c3964e078 100644 --- a/doc/gui/biomes.rst +++ b/docs/gui/biomes.rst @@ -1,17 +1,17 @@ -gui/biomes -========== - -.. dfhack-tool:: - :summary: Shows biomes map overlay. - :tags: fort inspection map - -This script shows biomes map overlay. -Hover over a biome entry in the list to get detailed info about it. - - -Usage ------ - -:: - - gui/biomes +gui/biomes +========== + +.. dfhack-tool:: + :summary: Shows biomes map overlay. + :tags: fort inspection map + +This script shows biomes map overlay. +Hover over a biome entry in the list to get detailed info about it. + + +Usage +----- + +:: + + gui/biomes From 8543bd87c64fa360f172e3c2e719e3c94fc8905e Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 7 May 2023 15:50:57 +0200 Subject: [PATCH 09/61] use linux line endings --- docs/gui/biomes.rst | 34 +++++++++++++++++----------------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/gui/biomes.rst b/docs/gui/biomes.rst index c3964e078..89a800bfb 100644 --- a/docs/gui/biomes.rst +++ b/docs/gui/biomes.rst @@ -1,17 +1,17 @@ -gui/biomes -========== - -.. dfhack-tool:: - :summary: Shows biomes map overlay. - :tags: fort inspection map - -This script shows biomes map overlay. -Hover over a biome entry in the list to get detailed info about it. - - -Usage ------ - -:: - - gui/biomes +gui/biomes +========== + +.. dfhack-tool:: + :summary: Shows biomes map overlay. + :tags: fort inspection map + +This script shows biomes map overlay. +Hover over a biome entry in the list to get detailed info about it. + + +Usage +----- + +:: + + gui/biomes From 85c695bcc99bfcd56d09783c2cb801d2d0ac0dfd Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Thu, 11 May 2023 11:56:00 +0200 Subject: [PATCH 10/61] Update docs/gui/biomes.rst Co-authored-by: Myk --- docs/gui/biomes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/biomes.rst b/docs/gui/biomes.rst index 89a800bfb..777ee7e43 100644 --- a/docs/gui/biomes.rst +++ b/docs/gui/biomes.rst @@ -2,7 +2,7 @@ gui/biomes ========== .. dfhack-tool:: - :summary: Shows biomes map overlay. + :summary: Visualize and inspect biome regions on the map. :tags: fort inspection map This script shows biomes map overlay. From 03f071e94ec460ad06bb21c17e00593bc3d2b876 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Thu, 11 May 2023 11:56:15 +0200 Subject: [PATCH 11/61] Update docs/gui/biomes.rst Co-authored-by: Myk --- docs/gui/biomes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/biomes.rst b/docs/gui/biomes.rst index 777ee7e43..899d345f4 100644 --- a/docs/gui/biomes.rst +++ b/docs/gui/biomes.rst @@ -5,7 +5,7 @@ gui/biomes :summary: Visualize and inspect biome regions on the map. :tags: fort inspection map -This script shows biomes map overlay. +This script shows the boundaries of the biome regions on the map. Hover over a biome entry in the list to get detailed info about it. From 36fb3311ce5add7d2b2edcb84aef22164214e106 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Thu, 11 May 2023 11:56:23 +0200 Subject: [PATCH 12/61] Update docs/gui/biomes.rst Co-authored-by: Myk --- docs/gui/biomes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/gui/biomes.rst b/docs/gui/biomes.rst index 899d345f4..550b0b30f 100644 --- a/docs/gui/biomes.rst +++ b/docs/gui/biomes.rst @@ -14,4 +14,4 @@ Usage :: - gui/biomes + gui/biomes From ee5151ca8dc4d17ad2710cbcf2bac501ffbdf606 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Thu, 11 May 2023 20:17:42 +0200 Subject: [PATCH 13/61] minor changes --- gui/biomes.lua | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index bd252b59d..47f4e97b9 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -1,4 +1,4 @@ --- Show biomes on the map. +-- Visualize and inspect biome regions on the map. local RELOAD = false -- set to true to help with debugging @@ -6,8 +6,11 @@ local gui = require('gui') local widgets = require('gui.widgets') local guidm = require('gui.dwarfmode') -local TILE_HIGHLIGHTED = 2585 -- yellow-ish indicator -local TILE_STARTING_SYMBOL = 353 -- `a` +local TILE_HIGHLIGHTED = dfhack.textures.getOnOffTexposStart() -- yellow-ish indicator +if TILE_HIGHLIGHTED < 0 then -- use a fallback + TILE_HIGHLIGHTED = 88 -- `X` +end +local TILE_STARTING_SYMBOL = 97 -- `a` local biomeTypeNames = { MOUNTAIN = "Mountain", @@ -338,6 +341,7 @@ if RELOAD then BiomeVisualizer = nil end BiomeVisualizer = defclass(BiomeVisualizer, gui.ZScreen) BiomeVisualizer.ATTRS{ focus_path='BiomeVisualizer', + pass_movement_keys=true, } function BiomeVisualizer:init() From cc3b8035ff0d6043cce67c76431b014caab5d445 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Thu, 11 May 2023 21:47:24 +0200 Subject: [PATCH 14/61] always gather biome info at the very bottom first --- gui/biomes.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index 47f4e97b9..a6ba47da2 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -136,7 +136,9 @@ local function gatherBiomeInfo(z) --end end -gatherBiomeInfo() +-- always gather info at the very bottom first: this ensures the important biomes are +-- always in the same order (high up in the air strange things happen) +gatherBiomeInfo(0) -------------------------------------------------------------------------------- From cd63dedad889e8ea204af71a805e28fafb3c54fc Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Thu, 11 May 2023 22:24:45 +0200 Subject: [PATCH 15/61] no longer resizable, tooltip inside the panel --- gui/biomes.lua | 112 ++++++++++++++++++++----------------------------- 1 file changed, 46 insertions(+), 66 deletions(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index a6ba47da2..3619dad05 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -145,15 +145,16 @@ gatherBiomeInfo(0) local TITLE = "Biomes" if RELOAD then BiomeVisualizerLegend = nil end +local DEFAULT_LIST_HEIGHT = 5 BiomeVisualizerLegend = defclass(BiomeVisualizerLegend, widgets.Window) BiomeVisualizerLegend.ATTRS { frame_title=TITLE, frame_inset=0, - resizable=true, + resizable=false, resize_min={h=5, w = 14}, frame = { w = 47, - h = 10, + h = DEFAULT_LIST_HEIGHT + 2 + 15, r = 2, t = 18, }, @@ -180,15 +181,35 @@ local function GetBiomeName(biome, biomeTypeId) end function BiomeVisualizerLegend:init() - self:addviews{ - widgets.List{ - view_id = 'list', - frame = { t = 0 }, - icon_width = 1, - text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, -- this makes selection stand out more - on_select = self:callback('onSelectEntry'), + local list = widgets.List{ + view_id = 'list', + frame = { t = 0, h = DEFAULT_LIST_HEIGHT, w = self.frame.w }, + frame_inset = 0, + icon_width = 1, + text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, -- this makes selection stand out more + on_select = self:callback('onSelectEntry'), + } + local tooltip_panel = widgets.Panel{ + view_id='tooltip_panel', + autoarrange_subviews=true, + frame = { t = list.frame.h + 1, h = 15 }, + frame_style=gui.INTERIOR_FRAME, + frame_background=gui.CLEAR_PEN, + subviews={ + widgets.Label{ + view_id='label', + text_to_wrap='', + scroll_keys={}, + }, }, } + self:addviews{ + list, + tooltip_panel, + } + + self.list = list + self.tooltip_panel = tooltip_panel self:UpdateChoices() end @@ -208,10 +229,13 @@ end function BiomeVisualizerLegend:onSelectEntry(idx, option) self.SelectedIndex = idx + self.SelectedOption = option + + self:ShowTooltip(option) end function BiomeVisualizerLegend:UpdateChoices() - local choices = self.subviews.list:getChoices() or {} + local choices = self.list:getChoices() or {} for i = #choices + 1, #biomeList do local biomeExt = biomeList[i] table.insert(choices, { @@ -221,14 +245,14 @@ function BiomeVisualizerLegend:UpdateChoices() biome = biomeExt.biome, }) end - self.subviews.list:setChoices(choices) + self.list:setChoices(choices) end do -- implementation of onMouseHoverEntry(idx, option) function BiomeVisualizerLegend:onRenderFrame(dc, rect) BiomeVisualizerLegend.super.onRenderFrame(self, dc, rect) - local list = self.subviews.list + local list = self.list local currentHoverIx = list:getIdxUnderMouse() local oldIx = self.HoverIndex if currentHoverIx ~= oldIx then @@ -246,12 +270,7 @@ local function add_field_text(lines, biome, field_name) lines[#lines+1] = NEWLINE end -function BiomeVisualizerLegend:onMouseHoverEntry(idx, option) - if not idx then - self:ShowTooltip(nil) - return - end - +local function get_tooltip_text(option) local text = {} text[#text+1] = ("type: %s"):format(df.biome_type[option.biomeTypeId]) text[#text+1] = NEWLINE @@ -280,61 +299,22 @@ function BiomeVisualizerLegend:onMouseHoverEntry(idx, option) text[#text+1] = NEWLINE end - self:ShowTooltip(text) + return text end -function BiomeVisualizerLegend:ShowTooltip(text) - self.TooltipWindow = self.TooltipWindow or self.parent_view.subviews.legend_tooltip - self.TooltipWindow:ShowTooltip(text) -end - --------------------------------------------------------------------------------- - -if RELOAD then TooltipWindow = nil end -TooltipWindow = defclass(TooltipWindow, widgets.Window) - -local function calc_tooltip_frame(frame) - local frame = copyall(frame) - frame.t = frame.t + frame.h - frame.h = 15 - return frame +function BiomeVisualizerLegend:onMouseHoverEntry(idx, option) + self:ShowTooltip(option or self.SelectedOption) end -TooltipWindow.ATTRS { - visible = false, - frame_title=DEFAULT_NIL, - frame_inset=0, - resizable=false, - frame_style = gui.PANEL_FRAME, - autoarrange_subviews = true, - autoarrange_gap = 0, - frame = calc_tooltip_frame(BiomeVisualizerLegend.ATTRS.frame), - -- - parent_window = DEFAULT_NIL, -} +function BiomeVisualizerLegend:ShowTooltip(option) + local text = get_tooltip_text(option) -function TooltipWindow:init() - self:addviews{ - widgets.Label{ - view_id = 'label', - frame = {t = 0, h = 1000}, - auto_height = false, - --wtf??? without this the label is always a single line - text={NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,NEWLINE,} - }, - } -end + local tooltip_panel = self.tooltip_panel + local lbl = tooltip_panel.subviews.label -function TooltipWindow:ShowTooltip(text) - if not text or text == "" or not self.parent_window then - self.visible = false - return - end - - local parent = self.parent_window - local lbl = self.subviews.label lbl:setText(text) - self.visible = true + + -- tooltip_panel:updateLayout() end -------------------------------------------------------------------------------- From 1c14eafa557ecde9454b7a1d558ba4bfd884249a Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Thu, 11 May 2023 22:26:25 +0200 Subject: [PATCH 16/61] remove commented out code --- gui/biomes.lua | 2 -- 1 file changed, 2 deletions(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index 3619dad05..c1ad8f966 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -313,8 +313,6 @@ function BiomeVisualizerLegend:ShowTooltip(option) local lbl = tooltip_panel.subviews.label lbl:setText(text) - - -- tooltip_panel:updateLayout() end -------------------------------------------------------------------------------- From b03ae09878eeb08d14757aa9330f4e4df0beb2a4 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Thu, 11 May 2023 22:42:46 +0200 Subject: [PATCH 17/61] clarify a comment --- gui/biomes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index c1ad8f966..1f101262e 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -1,6 +1,6 @@ -- Visualize and inspect biome regions on the map. -local RELOAD = false -- set to true to help with debugging +local RELOAD = false -- set to true when actively working on this script local gui = require('gui') local widgets = require('gui.widgets') From 10652752a3e06fe04146e4014177b825b84bfaff Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 13 May 2023 11:44:21 +0200 Subject: [PATCH 18/61] make resizable again, other small changes --- gui/biomes.lua | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index 1f101262e..8a9c6f11c 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -6,6 +6,9 @@ local gui = require('gui') local widgets = require('gui.widgets') local guidm = require('gui.dwarfmode') +local INITIAL_LIST_HEIGHT = 5 +local INITIAL_INFO_HEIGHT = 15 + local TILE_HIGHLIGHTED = dfhack.textures.getOnOffTexposStart() -- yellow-ish indicator if TILE_HIGHLIGHTED < 0 then -- use a fallback TILE_HIGHLIGHTED = 88 -- `X` @@ -145,16 +148,16 @@ gatherBiomeInfo(0) local TITLE = "Biomes" if RELOAD then BiomeVisualizerLegend = nil end -local DEFAULT_LIST_HEIGHT = 5 BiomeVisualizerLegend = defclass(BiomeVisualizerLegend, widgets.Window) BiomeVisualizerLegend.ATTRS { frame_title=TITLE, frame_inset=0, - resizable=false, + resizable=true, resize_min={h=5, w = 14}, frame = { w = 47, - h = DEFAULT_LIST_HEIGHT + 2 + 15, + h = INITIAL_LIST_HEIGHT + 2 + INITIAL_INFO_HEIGHT, + -- just under the minimap: r = 2, t = 18, }, @@ -183,7 +186,7 @@ end function BiomeVisualizerLegend:init() local list = widgets.List{ view_id = 'list', - frame = { t = 0, h = DEFAULT_LIST_HEIGHT, w = self.frame.w }, + frame = { t = 0, b = INITIAL_INFO_HEIGHT + 1 }, frame_inset = 0, icon_width = 1, text_pen = { fg = COLOR_GREY, bg = COLOR_BLACK }, -- this makes selection stand out more @@ -192,7 +195,7 @@ function BiomeVisualizerLegend:init() local tooltip_panel = widgets.Panel{ view_id='tooltip_panel', autoarrange_subviews=true, - frame = { t = list.frame.h + 1, h = 15 }, + frame = { b = 0, h = INITIAL_INFO_HEIGHT }, frame_style=gui.INTERIOR_FRAME, frame_background=gui.CLEAR_PEN, subviews={ @@ -326,8 +329,7 @@ BiomeVisualizer.ATTRS{ function BiomeVisualizer:init() local legend = BiomeVisualizerLegend{view_id = 'legend'} - local legend_tooltip = TooltipWindow{view_id = 'legend_tooltip', parent_window = legend} - self:addviews{legend, legend_tooltip} + self:addviews{legend} end function BiomeVisualizer:onRenderFrame(dc, rect) @@ -371,6 +373,7 @@ end if RELOAD and view then view:dismiss() + -- view is nil now end view = view and view:raise() or BiomeVisualizer{}:show() From fc38f8302d71f8e36c7d6dceeeb43a5de0126b07 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 20 May 2023 18:37:38 +0200 Subject: [PATCH 19/61] make the map overlay blink in text mode Other minor changes Co-authored-by: Myk --- gui/biomes.lua | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index 8a9c6f11c..71285c971 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -85,7 +85,7 @@ local function gatherBiomeInfo(z) local maxX, maxY, maxZ = dfhack.maps.getTileSize() maxX = maxX - 1; maxY = maxY - 1; maxZ = maxZ - 1 - local z = z or df.global.window_z + z = z or df.global.window_z --for z = 0, maxZ do for y = 0, maxY do @@ -274,6 +274,10 @@ local function add_field_text(lines, biome, field_name) end local function get_tooltip_text(option) + if not option then + return "" + end + local text = {} text[#text+1] = ("type: %s"):format(df.biome_type[option.biomeTypeId]) text[#text+1] = NEWLINE @@ -335,6 +339,10 @@ end function BiomeVisualizer:onRenderFrame(dc, rect) BiomeVisualizer.super.onRenderFrame(self, dc, rect) + if not dfhack.screen.inGraphicsMode() and not gui.blink_visible(500) then + return + end + local z = df.global.window_z if not biomesMap[z] then gatherBiomeInfo(z) From 0b37a9a7ce1addabcbe5ee6a3fc312a724700b3a Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 31 Dec 2023 12:10:30 +0100 Subject: [PATCH 20/61] use new textures API --- gui/biomes.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index 71285c971..470babcef 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -9,7 +9,8 @@ local guidm = require('gui.dwarfmode') local INITIAL_LIST_HEIGHT = 5 local INITIAL_INFO_HEIGHT = 15 -local TILE_HIGHLIGHTED = dfhack.textures.getOnOffTexposStart() -- yellow-ish indicator +local textures = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12, true) +local TILE_HIGHLIGHTED = dfhack.textures.getTexposByHandle(textures[1]) -- yellow-ish indicator if TILE_HIGHLIGHTED < 0 then -- use a fallback TILE_HIGHLIGHTED = 88 -- `X` end From 67f40c55b88cee25537c90836045253cfd3b7ba0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 6 Jan 2024 01:36:50 -0800 Subject: [PATCH 21/61] outline for new gui/teleport --- docs/gui/teleport.rst | 11 +- gui/teleport.lua | 389 +++++++++++++++++++++++++++++++++--------- 2 files changed, 316 insertions(+), 84 deletions(-) diff --git a/docs/gui/teleport.rst b/docs/gui/teleport.rst index 82d15abeb..29c1e5d0e 100644 --- a/docs/gui/teleport.rst +++ b/docs/gui/teleport.rst @@ -2,11 +2,14 @@ gui/teleport ============ .. dfhack-tool:: - :summary: Teleport a unit anywhere. - :tags: unavailable + :summary: Teleport units anywhere. + :tags: fort armok units -This tool is a front-end for the `teleport` tool. It allows you to interactively -choose a unit to teleport and a destination tile using the in-game cursor. +This tool allows you to interactively select units to teleport by drawing boxes +around them on the map. Double clicking on a destination tile will teleport the selected units there. + +If a unit is already selected in the UI when you run `gui/teleport`, it will be +pre-selected for teleport. Usage ----- diff --git a/gui/teleport.lua b/gui/teleport.lua index 0a29d32f0..0c2741c96 100644 --- a/gui/teleport.lua +++ b/gui/teleport.lua @@ -1,116 +1,345 @@ --- A front-end for the teleport script +local gui = require('gui') +local guidm = require('gui.dwarfmode') +local widgets = require('gui.widgets') ---[====[ - -gui/teleport -============ - -A front-end for the `teleport` script that allows choosing a unit and destination -using the in-game cursor. - -]====] - -guidm = require 'gui.dwarfmode' -widgets = require 'gui.widgets' +local function get_dims(pos1, pos2) + local width, height, depth = math.abs(pos1.x - pos2.x) + 1, + math.abs(pos1.y - pos2.y) + 1, + math.abs(pos1.z - pos2.z) + 1 + return width, height, depth +end -function uiMultipleUnits() - return #df.global.game.unit_cursor.list > 1 +local function is_good_unit(unit, include) + if not unit then return false end + if item.flags.forbid and not include.forbidden then return false end + if item.flags.in_job and not include.in_job then return false end + if item.flags.trader and not include.trader then return false end + return true end -TeleportSidebar = defclass(TeleportSidebar, guidm.MenuOverlay) +----------------- +-- Teleport +-- -TeleportSidebar.ATTRS = { - sidebar_mode=df.ui_sidebar_mode.ViewUnits, +Teleport = defclass(Teleport, widgets.Window) +Teleport.ATTRS { + frame_title='Teleport', + frame={w=48, h=28, r=2, t=18}, + resizable=true, + resize_min={h=10}, + autoarrange_subviews=true, + autoarrange_gap=1, } -function TeleportSidebar:init() +function Teleport:init() + self.mark = nil + self.prev_help_text = '' + self:reset_selected_state() -- sets self.selected_* + self:refresh_dump_items() -- sets self.dump_items + self:reset_double_click() -- sets self.last_map_click_ms and self.last_map_click_pos + self:addviews{ + widgets.WrappedLabel{ + frame={l=0}, + text_to_wrap=self:callback('get_help_text'), + }, + widgets.Panel{ + frame={h=2}, + subviews={ + widgets.Label{ + frame={l=0, t=0}, + text={ + 'Selected area: ', + {text=self:callback('get_selection_area_text')} + }, + }, + }, + visible=function() return self.mark end, + }, + widgets.HotkeyLabel{ + frame={l=0}, + label='Teleport to tile under mouse cursor', + key='CUSTOM_CTRL_T', + auto_width=true, + on_activate=self:callback('do_teleport'), + enabled=function() return dfhack.gui.getMousePos() end, + }, + widgets.ResizingPanel{ + autoarrange_subviews=true, + subviews={ + widgets.ToggleHotkeyLabel{ + view_id='include_citizens', + frame={l=0}, + label='Include citizen units', + key='CUSTOM_CTRL_U', + auto_width=true, + initial_option=true, + }, + widgets.ToggleHotkeyLabel{ + view_id='include_friendly', + frame={l=0}, + label='Include friendly units', + key='CUSTOM_CTRL_F', + auto_width=true, + initial_option=true, + }, + widgets.ToggleHotkeyLabel{ + view_id='include_hostile', + frame={l=0}, + label='Include hostile units', + key='CUSTOM_CTRL_H', + auto_width=true, + initial_option=true, + }, + }, + }, widgets.Label{ - frame = {b=1, l=1}, - text = { - {key = 'UNITJOB_ZOOM_CRE', - text = ': Zoom to unit, ', - on_activate = self:callback('zoom_unit'), - enabled = function() return self.unit end}, - {key = 'UNITVIEW_NEXT', text = ': Next', - on_activate = self:callback('next_unit'), - enabled = uiMultipleUnits}, - NEWLINE, - NEWLINE, - {key = 'SELECT', text = ': Choose, ', on_activate = self:callback('choose')}, - {key = 'LEAVESCREEN', text = ': Back', on_activate = self:callback('back')}, - NEWLINE, - {key = 'LEAVESCREEN_ALL', text = ': Exit to map', on_activate = self:callback('dismiss')}, + text={ + {text=function() return #self.selected_units.list end} + ' selected units:' }, }, - } - self.in_pick_pos = false + widgets.List{ + view_id='list', + frame={l=0, r=0, b=2} + }, + widgets.HotkeyLabel{ + frame={l=0}, + key='CUSTOM_R', + label='Remove selected unit from list', + auto_width=true, + on_activate=self:callback('remove_unit'), + enabled=function() return #self.selected_items.list > 0 end, + }, + widgets.HotkeyLabel{ + frame={l=0}, + key='CUSTOM_SHIFT_R', + label='Remove all selected units', + auto_width=true, + on_activate=self:callback('reset_selected_state'), + enabled=function() return #self.selected_items.list > 0 end, + }, +} end -function TeleportSidebar:choose() - if not self.in_pick_pos then - self.in_pick_pos = true - else - dfhack.units.teleport(self.unit, xyz2pos(pos2xyz(df.global.cursor))) - self:dismiss() +function Teleport:reset_double_click() + self.last_map_click_ms = 0 + self.last_map_click_pos = {} +end + +function Teleport:reset_selected_state() + self.selected_items = {list={}, set={}} + self.selected_coords = {} -- z -> y -> x -> true + self.selected_bounds = {} -- z -> bounds rect + if next(self.subviews) then + self:updateLayout() + end +end + +function Teleport:get_include() + local include = {forbidden=false, in_job=false, trader=false} + if next(self.subviews) then + include.forbidden = self.subviews.include_forbidden:getOptionValue() + include.in_job = self.subviews.include_in_job:getOptionValue() + include.trader = self.subviews.include_trader:getOptionValue() + end + return include +end + +function Teleport:refresh_dump_items() + local dump_items = {} + local include = self:get_include() + for _,item in ipairs(df.global.world.items.all) do + if not is_good_item(item, include) then goto continue end + if item.flags.dump then + table.insert(dump_items, item) + end + ::continue:: + end + self.dump_items = dump_items + if next(self.subviews) then + self:updateLayout() end end -function TeleportSidebar:back() - if self.in_pick_pos then - self.in_pick_pos = false +function Teleport:get_help_text() + local ret = 'Double click on a tile to teleport' + if #self.selected_items.list > 0 then + ret = ('%s %d highlighted item(s).'):format(ret, #self.selected_items.list) else - self:dismiss() + ret = ('%s %d item(s) marked for dumping.'):format(ret, #self.dump_items) + end + if ret ~= self.prev_help_text then + self.prev_help_text = ret end + return ret end -function TeleportSidebar:zoom_unit() - df.global.cursor:assign(xyz2pos(pos2xyz(self.unit.pos))) - self:getViewport():centerOn(self.unit.pos):set() +function Teleport:get_selection_area_text() + local mark = self.mark + if not mark then return '' end + local cursor = dfhack.gui.getMousePos() or {x=mark.x, y=mark.y, z=df.global.window_z} + return ('%dx%dx%d'):format(get_dims(mark, cursor)) end -function TeleportSidebar:next_unit() - self:sendInputToParent('UNITVIEW_NEXT') +function Teleport:get_bounds(cursor, mark) + cursor = cursor or self.mark + mark = mark or self.mark or cursor + if not mark then return end + + return { + x1=math.min(cursor.x, mark.x), + x2=math.max(cursor.x, mark.x), + y1=math.min(cursor.y, mark.y), + y2=math.max(cursor.y, mark.y), + z1=math.min(cursor.z, mark.z), + z2=math.max(cursor.z, mark.z) + } +end + +function Teleport:select_items_in_block(block, bounds) + local include = self:get_include() + for _,item_id in ipairs(block.items) do + local item = df.item.find(item_id) + if not is_good_item(item, include) then + goto continue + end + local x, y, z = dfhack.items.getPosition(item) + if not x then goto continue end + if not self.selected_items.set[item_id] and + x >= bounds.x1 and x <= bounds.x2 and + y >= bounds.y1 and y <= bounds.y2 then + self.selected_items.set[item_id] = true + table.insert(self.selected_items.list, item) + ensure_key(ensure_key(self.selected_coords, z), y)[x] = true + local selected_bounds = ensure_key(self.selected_bounds, z, + {x1=x, x2=x, y1=y, y2=y}) + selected_bounds.x1 = math.min(selected_bounds.x1, x) + selected_bounds.x2 = math.max(selected_bounds.x2, x) + selected_bounds.y1 = math.min(selected_bounds.y1, y) + selected_bounds.y2 = math.max(selected_bounds.y2, y) + end + ::continue:: + end end -function TeleportSidebar:onRenderBody(p) - p:seek(1, 1):pen(COLOR_WHITE) - if self.in_pick_pos then - p:string('Select destination'):newline(1):newline(1) +function Teleport:select_box(bounds) + if not bounds then return end + local seen_blocks = {} + for z=bounds.z1,bounds.z2 do + for y=bounds.y1,bounds.y2 do + for x=bounds.x1,bounds.x2 do + local block = dfhack.maps.getTileBlock(xyz2pos(x, y, z)) + local block_str = tostring(block) + if not seen_blocks[block_str] then + seen_blocks[block_str] = true + self:select_items_in_block(block, bounds) + end + end + end + end +end - local cursor = df.global.cursor - local block = dfhack.maps.getTileBlock(pos2xyz(cursor)) - if block then - p:string(df.tiletype[block.tiletype[cursor.x % 16][cursor.y % 16]], COLOR_CYAN) - else - p:string('Unknown tile', COLOR_RED) +function Teleport:onInput(keys) + if Teleport.super.onInput(self, keys) then return true end + if keys._MOUSE_R and self.mark then + self.mark = nil + self:updateLayout() + return true + elseif keys._MOUSE_L then + if self:getMouseFramePos() then return true end + local pos = dfhack.gui.getMousePos() + if not pos then + self:reset_double_click() + return false end - else - self.unit = dfhack.gui.getAnyUnit(self._native.parent) - p:string('Select unit:'):newline(1):newline(1) - if self.unit then - local name = dfhack.TranslateName(dfhack.units.getVisibleName(self.unit)) - p:string(name) - if name ~= '' then p:newline(1) end - p:string(dfhack.units.getProfessionName(self.unit), dfhack.units.getProfessionColor(self.unit)) - p:newline(1) - else - p:string('No unit selected', COLOR_LIGHTRED) + local now_ms = dfhack.getTickCount() + if same_xyz(pos, self.last_map_click_pos) and + now_ms - self.last_map_click_ms <= widgets.DOUBLE_CLICK_MS then + self:reset_double_click() + self:do_teleport(pos) + self.mark = nil + self:updateLayout() + return true + end + self.last_map_click_ms = now_ms + self.last_map_click_pos = pos + if self.mark then + self:select_box(self:get_bounds(pos)) + self:reset_double_click() + self.mark = nil + self:updateLayout() + return true end + self.mark = pos + self:updateLayout() + return true end end -function TeleportSidebar:onInput(keys) - TeleportSidebar.super.onInput(self, keys) - TeleportSidebar.super.propagateMoveKeys(self, keys) +local to_pen = dfhack.pen.parse +local CURSOR_PEN = to_pen{ch='o', fg=COLOR_BLUE, + tile=dfhack.screen.findGraphicsTile('CURSORS', 5, 22)} +local BOX_PEN = to_pen{ch='X', fg=COLOR_GREEN, + tile=dfhack.screen.findGraphicsTile('CURSORS', 0, 0)} +local SELECTED_PEN = to_pen{ch='I', fg=COLOR_GREEN, + tile=dfhack.screen.findGraphicsTile('CURSORS', 1, 2)} + +function Teleport:onRenderFrame(dc, rect) + Teleport.super.onRenderFrame(self, dc, rect) + + local highlight_coords = self.selected_coords[df.global.window_z] + if highlight_coords then + local function get_overlay_pen(pos) + if safe_index(highlight_coords, pos.y, pos.x) then + return SELECTED_PEN + end + end + guidm.renderMapOverlay(get_overlay_pen, self.selected_bounds[df.global.window_z]) + end + + -- draw selection box and cursor (blinking when in ascii mode) + local cursor = dfhack.gui.getMousePos() + local selection_bounds = self:get_bounds(cursor) + if selection_bounds and (dfhack.screen.inGraphicsMode() or gui.blink_visible(500)) then + guidm.renderMapOverlay( + function() return self.mark and BOX_PEN or CURSOR_PEN end, + selection_bounds) + end end -function TeleportSidebar:onGetSelectedUnit() - return self.unit +function Teleport:do_teleport(pos) + pos = pos or dfhack.gui.getMousePos() + if not pos then return end + print(('teleporting %d units'):format(#self.unit_ids)) + for _,unid in ipairs(self.unit_ids) do + local unit = df.unit.find(unid) + if unit then + dfhack.units.teleport(unit, pos) + end + end + self.unit_ids = {} + self:reset_selected_state() + self:updateLayout() +end + +----------------- +-- TeleportScreen +-- + +TeleportScreen = defclass(TeleportScreen, gui.ZScreen) +TeleportScreen.ATTRS { + focus_path='autodump', + pass_movement_keys=true, + pass_mouse_clicks=false, +} + +function TeleportScreen:init() + self:addviews{Teleport{}} end -if not dfhack.isMapLoaded() then - qerror('This script requires a fortress map to be loaded') +function TeleportScreen:onDismiss() + view = nil end -TeleportSidebar():show() +view = view and view:raise() or TeleportScreen{}:show() From 7885ac3a9734382d658ad07322a7f8d0a897083a Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 6 Jan 2024 16:24:17 +0100 Subject: [PATCH 22/61] use special textures for map overlay tiles --- gui/biomes.lua | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index 470babcef..bd112d1f7 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -9,12 +9,20 @@ local guidm = require('gui.dwarfmode') local INITIAL_LIST_HEIGHT = 5 local INITIAL_INFO_HEIGHT = 15 -local textures = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12, true) -local TILE_HIGHLIGHTED = dfhack.textures.getTexposByHandle(textures[1]) -- yellow-ish indicator +local texturesOnOff8x12 = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12, true) +local LIST_ITEM_HIGHLIGHTED = dfhack.textures.getTexposByHandle(texturesOnOff8x12[1]) -- yellow-ish indicator + +local texturesOnOff = dfhack.textures.loadTileset('hack/data/art/on-off - top-left.png', 32, 32, true) +local TILE_HIGHLIGHTED = dfhack.textures.getTexposByHandle(texturesOnOff[1]) -- yellow-ish indicator if TILE_HIGHLIGHTED < 0 then -- use a fallback TILE_HIGHLIGHTED = 88 -- `X` end -local TILE_STARTING_SYMBOL = 97 -- `a` + +local texturesSmallLetters = dfhack.textures.loadTileset('hack/data/art/curses_small_letters - top-left.png', 32, 32, true) +local TILE_STARTING_SYMBOL = dfhack.textures.getTexposByHandle(texturesSmallLetters[1]) +if TILE_STARTING_SYMBOL < 0 then -- use a fallback + TILE_STARTING_SYMBOL = 97 -- `a` +end local biomeTypeNames = { MOUNTAIN = "Mountain", @@ -218,7 +226,7 @@ function BiomeVisualizerLegend:init() self:UpdateChoices() end -local PEN_ACTIVE_ICON = dfhack.pen.parse{tile=TILE_HIGHLIGHTED} +local PEN_ACTIVE_ICON = dfhack.pen.parse{tile=LIST_ITEM_HIGHLIGHTED} local PEN_NO_ICON = nil function BiomeVisualizerLegend:get_icon_pen_callback(ix) From 746aacfe6598586e1ead8f63685a5d24f6652b7b Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 6 Jan 2024 16:24:45 +0100 Subject: [PATCH 23/61] adjust tooltip based on map tile hovered over --- gui/biomes.lua | 37 ++++++++++++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index bd112d1f7..981a1e837 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -239,6 +239,18 @@ function BiomeVisualizerLegend:get_icon_pen_callback(ix) end end +function BiomeVisualizerLegend:get_text_pen_callback(ix) + return function () + if self.MapHoverIndex == ix then + return self.SelectedIndex == ix + and { fg = COLOR_BLACK, bg = COLOR_LIGHTCYAN } + or { fg = COLOR_BLACK, bg = COLOR_GREY } + else + return nil + end + end +end + function BiomeVisualizerLegend:onSelectEntry(idx, option) self.SelectedIndex = idx self.SelectedOption = option @@ -251,7 +263,10 @@ function BiomeVisualizerLegend:UpdateChoices() for i = #choices + 1, #biomeList do local biomeExt = biomeList[i] table.insert(choices, { - text = ([[%s: %s]]):format(biomeExt.char, GetBiomeName(biomeExt.biome, biomeExt.typeId)), + text = {{ + pen = self:get_text_pen_callback(#choices+1), + text = ([[%s: %s]]):format(biomeExt.char, GetBiomeName(biomeExt.biome, biomeExt.typeId)), + }}, icon = self:get_icon_pen_callback(#choices+1), biomeTypeId = biomeExt.typeId, biome = biomeExt.biome, @@ -331,6 +346,26 @@ function BiomeVisualizerLegend:ShowTooltip(option) lbl:setText(text) end +function BiomeVisualizerLegend:onRenderBody(painter) + local thisPos = self:getMouseFramePos() + local pos = dfhack.gui.getMousePos() + + if not thisPos and pos then + local N = safe_index(biomesMap, pos.z, pos.y, pos.x) + if N then + local choices = self.list:getChoices() + local option = choices[N] + + self.MapHoverIndex = N + self:ShowTooltip(option) + end + else + self.MapHoverIndex = nil + end + + BiomeVisualizerLegend.super.onRenderBody(self, painter) +end + -------------------------------------------------------------------------------- if RELOAD then BiomeVisualizer = nil end From 9481147a662cd58783079261c124e81f121324dc Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 6 Jan 2024 16:28:02 +0100 Subject: [PATCH 24/61] remove trailing whitespace --- gui/biomes.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index 981a1e837..e16a9f5fe 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -349,7 +349,7 @@ end function BiomeVisualizerLegend:onRenderBody(painter) local thisPos = self:getMouseFramePos() local pos = dfhack.gui.getMousePos() - + if not thisPos and pos then local N = safe_index(biomesMap, pos.z, pos.y, pos.x) if N then From bea7f703a49be5416fc9f88c79aa8b9fb7cf15d9 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sat, 6 Jan 2024 16:35:01 +0100 Subject: [PATCH 25/61] add changelog entry --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 439ad2ec6..0a580e31d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,7 @@ Template for new versions: - `control-panel`: new commandline interface for control panel functions - `uniform-unstick`: (reinstated) force squad members to drop items that they picked up in the wrong order so they can get everything equipped properly - `gui/reveal`: temporarily unhide terrain and then automatically hide it again when you're ready to unpause +- `gui/biomes`: visualize and inspect biome regions on the map ## New Features - `uniform-unstick`: add overlay to the squad equipment screen to show a equipment conflict report and give you a one-click button to fix From 0ef6b48063f62bd9828010af0fc64bd2a1cf03c7 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 6 Jan 2024 13:20:26 -0800 Subject: [PATCH 26/61] finalize logic --- docs/gui/teleport.rst | 5 + gui/teleport.lua | 278 +++++++++++++++++++++++------------------- 2 files changed, 158 insertions(+), 125 deletions(-) diff --git a/docs/gui/teleport.rst b/docs/gui/teleport.rst index 29c1e5d0e..6222af4f8 100644 --- a/docs/gui/teleport.rst +++ b/docs/gui/teleport.rst @@ -11,6 +11,11 @@ around them on the map. Double clicking on a destination tile will teleport the If a unit is already selected in the UI when you run `gui/teleport`, it will be 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. + Usage ----- diff --git a/gui/teleport.lua b/gui/teleport.lua index 0c2741c96..60eca9142 100644 --- a/gui/teleport.lua +++ b/gui/teleport.lua @@ -1,7 +1,12 @@ local gui = require('gui') local guidm = require('gui.dwarfmode') +local utils = require('utils') local widgets = require('gui.widgets') +saved_citizens = saved_citizens or (saved_citizens == nil and true) +saved_friendly = saved_friendly or (saved_friendly == nil and true) +saved_hostile = saved_hostile or (saved_hostile == nil and true) + local function get_dims(pos1, pos2) local width, height, depth = math.abs(pos1.x - pos2.x) + 1, math.abs(pos1.y - pos2.y) + 1, @@ -9,12 +14,18 @@ local function get_dims(pos1, pos2) return width, height, depth end -local function is_good_unit(unit, include) +local function is_good_unit(include, unit) if not unit then return false end - if item.flags.forbid and not include.forbidden then return false end - if item.flags.in_job and not include.in_job then return false end - if item.flags.trader and not include.trader then return false end - return true + if dfhack.units.isDead(unit) or + not dfhack.units.isActive(unit) or + unit.flags1.caged + then + return false + end + if dfhack.units.isCitizen(unit) then return include.citizens end + local dangerous = dfhack.units.isDanger(unit) + if not dangerous then return include.friendly end + return include.hostile end ----------------- @@ -24,9 +35,9 @@ end Teleport = defclass(Teleport, widgets.Window) Teleport.ATTRS { frame_title='Teleport', - frame={w=48, h=28, r=2, t=18}, + frame={w=44, h=28, r=2, t=18}, resizable=true, - resize_min={h=10}, + resize_min={h=20}, autoarrange_subviews=true, autoarrange_gap=1, } @@ -35,9 +46,11 @@ function Teleport:init() self.mark = nil self.prev_help_text = '' self:reset_selected_state() -- sets self.selected_* - self:refresh_dump_items() -- sets self.dump_items self:reset_double_click() -- sets self.last_map_click_ms and self.last_map_click_pos + -- pre-add UI selected unit, if any + self:add_unit(dfhack.gui.getSelectedUnit(true)) + self:addviews{ widgets.WrappedLabel{ frame={l=0}, @@ -58,68 +71,82 @@ function Teleport:init() }, widgets.HotkeyLabel{ frame={l=0}, - label='Teleport to tile under mouse cursor', + label='Teleport units to mouse cursor', key='CUSTOM_CTRL_T', auto_width=true, on_activate=self:callback('do_teleport'), - enabled=function() return dfhack.gui.getMousePos() end, + enabled=function() + return dfhack.gui.getMousePos() and #self.selected_units.list > 0 + end, }, widgets.ResizingPanel{ autoarrange_subviews=true, subviews={ widgets.ToggleHotkeyLabel{ view_id='include_citizens', - frame={l=0}, - label='Include citizen units', - key='CUSTOM_CTRL_U', - auto_width=true, - initial_option=true, + frame={l=0, w=29}, + label='Include citizen units ', + key='CUSTOM_SHIFT_U', + initial_option=saved_citizens, + on_change=function(val) saved_citizens = val end, }, widgets.ToggleHotkeyLabel{ view_id='include_friendly', - frame={l=0}, + frame={l=0, w=29}, label='Include friendly units', - key='CUSTOM_CTRL_F', - auto_width=true, - initial_option=true, + key='CUSTOM_SHIFT_F', + initial_option=saved_friendly, + on_change=function(val) saved_friendly = val end, }, widgets.ToggleHotkeyLabel{ view_id='include_hostile', - frame={l=0}, - label='Include hostile units', - key='CUSTOM_CTRL_H', - auto_width=true, - initial_option=true, + frame={l=0, w=29}, + label='Include hostile units ', + key='CUSTOM_SHIFT_H', + initial_option=saved_hostile, + on_change=function(val) saved_hostile = val end, }, }, }, - widgets.Label{ - text={ - {text=function() return #self.selected_units.list end} - ' selected units:' - }, - }, - widgets.List{ - view_id='list', - frame={l=0, r=0, b=2} - }, - widgets.HotkeyLabel{ - frame={l=0}, - key='CUSTOM_R', - label='Remove selected unit from list', - auto_width=true, - on_activate=self:callback('remove_unit'), - enabled=function() return #self.selected_items.list > 0 end, - }, - widgets.HotkeyLabel{ - frame={l=0}, - key='CUSTOM_SHIFT_R', - label='Remove all selected units', - auto_width=true, - on_activate=self:callback('reset_selected_state'), - enabled=function() return #self.selected_items.list > 0 end, - }, -} + widgets.Panel{ + frame={t=10, b=0, l=0, r=0}, + frame_style=gui.FRAME_INTERIOR, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text='No selected units', + visible=function() return #self.selected_units.list == 0 end, + }, + widgets.Label{ + frame={t=0, l=0}, + text='Selected units:', + visible=function() return #self.selected_units.list > 0 end, + }, + widgets.List{ + view_id='list', + frame={t=2, l=0, r=0, b=3}, + }, + widgets.HotkeyLabel{ + frame={l=0, b=1}, + key='CUSTOM_SHIFT_R', + label='Remove unit from list', + auto_width=true, + on_activate=self:callback('remove_unit'), + enabled=function() return #self.selected_units.list > 0 end, + }, + widgets.HotkeyLabel{ + frame={l=0, b=0}, + key='CUSTOM_SHIFT_X', + label='Clear list', + auto_width=true, + on_activate=self:callback('reset_selected_state'), + enabled=function() return #self.selected_units.list > 0 end, + }, + } + } + } + + self:refresh_choices() end function Teleport:reset_double_click() @@ -127,52 +154,88 @@ function Teleport:reset_double_click() self.last_map_click_pos = {} end -function Teleport:reset_selected_state() - self.selected_items = {list={}, set={}} +function Teleport:update_coords(x, y, z) + ensure_keys(self.selected_coords, z, y)[x] = true + local selected_bounds = ensure_key(self.selected_bounds, z, + {x1=x, x2=x, y1=y, y2=y}) + selected_bounds.x1 = math.min(selected_bounds.x1, x) + selected_bounds.x2 = math.max(selected_bounds.x2, x) + selected_bounds.y1 = math.min(selected_bounds.y1, y) + selected_bounds.y2 = math.max(selected_bounds.y2, y) +end + +function Teleport:add_unit(unit) + if not unit then return end + local x, y, z = dfhack.units.getPosition(unit) + if not x then return end + if not self.selected_units.set[unit.id] then + self.selected_units.set[unit.id] = true + utils.insert_sorted(self.selected_units.list, unit, 'id') + self:update_coords(x, y, z) + end +end + +function Teleport:reset_selected_state(keep_units) + if not keep_units then + self.selected_units = {list={}, set={}} + end self.selected_coords = {} -- z -> y -> x -> true self.selected_bounds = {} -- z -> bounds rect + for _, unit in ipairs(self.selected_units.list) do + self:update_coords(dfhack.units.getPosition(unit)) + end if next(self.subviews) then self:updateLayout() + self:refresh_choices() end end -function Teleport:get_include() - local include = {forbidden=false, in_job=false, trader=false} - if next(self.subviews) then - include.forbidden = self.subviews.include_forbidden:getOptionValue() - include.in_job = self.subviews.include_in_job:getOptionValue() - include.trader = self.subviews.include_trader:getOptionValue() +function Teleport:refresh_choices() + local choices = {} + for _, unit in ipairs(self.selected_units.list) do + local suffix + if dfhack.units.isCitizen(unit) then suffix = ' citizen' + elseif dfhack.units.isDanger(unit) then suffix = ' hostile' + else suffix = ' friendly' + end + table.insert(choices, { + text=dfhack.units.getReadableName(unit)..suffix, + unit=unit + }) end - return include + table.sort(choices, function(a, b) return a.text < b.text end) + self.subviews.list:setChoices(choices) end -function Teleport:refresh_dump_items() - local dump_items = {} - local include = self:get_include() - for _,item in ipairs(df.global.world.items.all) do - if not is_good_item(item, include) then goto continue end - if item.flags.dump then - table.insert(dump_items, item) - end - ::continue:: - end - self.dump_items = dump_items +function Teleport:remove_unit() + local _, choice = self.subviews.list:getSelected() + if not choice then return end + self.selected_units.set[choice.unit.id] = nil + utils.erase_sorted_key(self.selected_units.list, choice.unit.id, 'id') + self:reset_selected_state(true) +end + +function Teleport:get_include() + local include = {citizens=false, friendly=false, hostile=false} if next(self.subviews) then - self:updateLayout() + include.citizens = self.subviews.include_citizens:getOptionValue() + include.friendly = self.subviews.include_friendly:getOptionValue() + include.hostile = self.subviews.include_hostile:getOptionValue() end + return include end function Teleport:get_help_text() - local ret = 'Double click on a tile to teleport' - if #self.selected_items.list > 0 then - ret = ('%s %d highlighted item(s).'):format(ret, #self.selected_items.list) - else - ret = ('%s %d item(s) marked for dumping.'):format(ret, #self.dump_items) + local help_text = 'Draw boxes around units to select' + local num_selected = #self.selected_units.list + if num_selected > 0 then + help_text = help_text .. + (', or double click on a tile to teleport %d selected unit(s).'):format(num_selected) end - if ret ~= self.prev_help_text then - self.prev_help_text = ret + if help_text ~= self.prev_help_text then + self.prev_help_text = help_text end - return ret + return help_text end function Teleport:get_selection_area_text() @@ -197,47 +260,15 @@ function Teleport:get_bounds(cursor, mark) } end -function Teleport:select_items_in_block(block, bounds) - local include = self:get_include() - for _,item_id in ipairs(block.items) do - local item = df.item.find(item_id) - if not is_good_item(item, include) then - goto continue - end - local x, y, z = dfhack.items.getPosition(item) - if not x then goto continue end - if not self.selected_items.set[item_id] and - x >= bounds.x1 and x <= bounds.x2 and - y >= bounds.y1 and y <= bounds.y2 then - self.selected_items.set[item_id] = true - table.insert(self.selected_items.list, item) - ensure_key(ensure_key(self.selected_coords, z), y)[x] = true - local selected_bounds = ensure_key(self.selected_bounds, z, - {x1=x, x2=x, y1=y, y2=y}) - selected_bounds.x1 = math.min(selected_bounds.x1, x) - selected_bounds.x2 = math.max(selected_bounds.x2, x) - selected_bounds.y1 = math.min(selected_bounds.y1, y) - selected_bounds.y2 = math.max(selected_bounds.y2, y) - end - ::continue:: - end -end - function Teleport:select_box(bounds) if not bounds then return end - local seen_blocks = {} - for z=bounds.z1,bounds.z2 do - for y=bounds.y1,bounds.y2 do - for x=bounds.x1,bounds.x2 do - local block = dfhack.maps.getTileBlock(xyz2pos(x, y, z)) - local block_str = tostring(block) - if not seen_blocks[block_str] then - seen_blocks[block_str] = true - self:select_items_in_block(block, bounds) - end - end - end + local filter = curry(is_good_unit, self:get_include()) + local selected_units = dfhack.units.getUnitsInBox( + bounds.x1, bounds.y1, bounds.z1, bounds.x2, bounds.y2, bounds.z2, filter) + for _,unit in ipairs(selected_units) do + self:add_unit(unit) end + self:refresh_choices() end function Teleport:onInput(keys) @@ -311,14 +342,10 @@ end function Teleport:do_teleport(pos) pos = pos or dfhack.gui.getMousePos() if not pos then return end - print(('teleporting %d units'):format(#self.unit_ids)) - for _,unid in ipairs(self.unit_ids) do - local unit = df.unit.find(unid) - if unit then - dfhack.units.teleport(unit, pos) - end + print(('teleporting %d units'):format(#self.selected_units.list)) + for _,unit in ipairs(self.selected_units.list) do + dfhack.units.teleport(unit, pos) end - self.unit_ids = {} self:reset_selected_state() self:updateLayout() end @@ -332,6 +359,7 @@ TeleportScreen.ATTRS { focus_path='autodump', pass_movement_keys=true, pass_mouse_clicks=false, + force_pause=true, } function TeleportScreen:init() From 769a079f00fa59bb9aba2efe32fa672eef683cc9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 6 Jan 2024 13:20:57 -0800 Subject: [PATCH 27/61] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 439ad2ec6..3724f39cd 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,7 @@ Template for new versions: - `control-panel`: new commandline interface for control panel functions - `uniform-unstick`: (reinstated) force squad members to drop items that they picked up in the wrong order so they can get everything equipped properly - `gui/reveal`: temporarily unhide terrain and then automatically hide it again when you're ready to unpause +- `gui/teleport`: mouse-driven interface for selecting and teleporting units ## New Features - `uniform-unstick`: add overlay to the squad equipment screen to show a equipment conflict report and give you a one-click button to fix From eea3db62efc3e75a067fb02b8721ee199a13a1ef Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 6 Jan 2024 15:13:46 -0800 Subject: [PATCH 28/61] add unit highlighting and zooming --- gui/teleport.lua | 55 ++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 9 deletions(-) diff --git a/gui/teleport.lua b/gui/teleport.lua index 60eca9142..8f9686da7 100644 --- a/gui/teleport.lua +++ b/gui/teleport.lua @@ -7,6 +7,8 @@ saved_citizens = saved_citizens or (saved_citizens == nil and true) saved_friendly = saved_friendly or (saved_friendly == nil and true) saved_hostile = saved_hostile or (saved_hostile == nil and true) +local indicator = df.global.game.main_interface.recenter_indicator_m + local function get_dims(pos1, pos2) local width, height, depth = math.abs(pos1.x - pos2.x) + 1, math.abs(pos1.y - pos2.y) + 1, @@ -35,7 +37,7 @@ end Teleport = defclass(Teleport, widgets.Window) Teleport.ATTRS { frame_title='Teleport', - frame={w=44, h=28, r=2, t=18}, + frame={w=45, h=28, r=2, t=18}, resizable=true, resize_min={h=20}, autoarrange_subviews=true, @@ -49,7 +51,12 @@ function Teleport:init() self:reset_double_click() -- sets self.last_map_click_ms and self.last_map_click_pos -- pre-add UI selected unit, if any - self:add_unit(dfhack.gui.getSelectedUnit(true)) + local initial_unit = dfhack.gui.getSelectedUnit(true) + if initial_unit then + self:add_unit(initial_unit) + end + -- close the view sheet panel (if it's open) so the player can see the map + df.global.game.main_interface.view_sheets.open = false self:addviews{ widgets.WrappedLabel{ @@ -124,24 +131,43 @@ function Teleport:init() }, widgets.List{ view_id='list', - frame={t=2, l=0, r=0, b=3}, + frame={t=2, l=0, r=0, b=4}, + on_select=function(_, choice) + if choice then + df.assign(indicator, xyz2pos(dfhack.units.getPosition(choice.unit))) + end + end, + on_submit=function(_, choice) + if choice then + local pos = xyz2pos(dfhack.units.getPosition(choice.unit)) + dfhack.gui.revealInDwarfmodeMap(pos, true, true) + end + end, }, widgets.HotkeyLabel{ frame={l=0, b=1}, key='CUSTOM_SHIFT_R', - label='Remove unit from list', + label='Deselect unit', auto_width=true, on_activate=self:callback('remove_unit'), enabled=function() return #self.selected_units.list > 0 end, }, widgets.HotkeyLabel{ - frame={l=0, b=0}, + frame={l=26, b=1}, key='CUSTOM_SHIFT_X', label='Clear list', auto_width=true, on_activate=self:callback('reset_selected_state'), enabled=function() return #self.selected_units.list > 0 end, }, + widgets.Label{ + frame={l=0, b=0}, + text={ + 'Click name or hit ', + {text='Enter', pen=COLOR_LIGHTGREEN}, + ' to zoom to unit', + }, + }, } } } @@ -193,10 +219,18 @@ end function Teleport:refresh_choices() local choices = {} for _, unit in ipairs(self.selected_units.list) do - local suffix - if dfhack.units.isCitizen(unit) then suffix = ' citizen' - elseif dfhack.units.isDanger(unit) then suffix = ' hostile' - else suffix = ' friendly' + local suffix = '' + if dfhack.units.isCitizen(unit) then suffix = ' (citizen)' + elseif dfhack.units.isDanger(unit) then suffix = ' (hostile)' + elseif dfhack.units.isMerchant(unit) or dfhack.units.isForest(unit) then + suffix = ' (merchant)' + elseif dfhack.units.isAnimal(unit) then + -- tame units will already have an annotation in the readable name + if not dfhack.units.isTame(unit) then + suffix = ' (wild)' + end + else + suffix = ' (friendly)' end table.insert(choices, { text=dfhack.units.getReadableName(unit)..suffix, @@ -322,6 +356,7 @@ function Teleport:onRenderFrame(dc, rect) local highlight_coords = self.selected_coords[df.global.window_z] if highlight_coords then local function get_overlay_pen(pos) + if same_xyz(indicator, pos) then return end if safe_index(highlight_coords, pos.y, pos.x) then return SELECTED_PEN end @@ -346,6 +381,7 @@ function Teleport:do_teleport(pos) for _,unit in ipairs(self.selected_units.list) do dfhack.units.teleport(unit, pos) end + indicator.x = -30000 self:reset_selected_state() self:updateLayout() end @@ -367,6 +403,7 @@ function TeleportScreen:init() end function TeleportScreen:onDismiss() + indicator.x = -30000 view = nil end From 9e8ca0e961f3c4c8101260ada1b9c5599ddb3c84 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 6 Jan 2024 15:57:11 -0800 Subject: [PATCH 29/61] warn (and skip) if unit squad is from another site --- uniform-unstick.lua | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/uniform-unstick.lua b/uniform-unstick.lua index aedd747e6..8ba40d6b8 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -30,9 +30,29 @@ local function get_item_pos(item) end end -local function get_squad_position(unit) +local function get_site_id() + local fort = df.global.world.world_data.active_site[0] + for _, el in ipairs(fort.entity_links) do + local he = df.historical_entity.find(el.entity_id) + if he and he.type == df.historical_entity_type.SiteGovernment then + return el.entity_id + end + end +end + +local site_id = get_site_id() + +local function get_squad_position(unit, unit_name) local squad = df.squad.find(unit.military.squad_id) - if not squad then return end + if squad then + if squad.entity_id ~= site_id then + print("WARNING: Unit " .. unit_name .. " is a member of a squad in another site!" .. + " You can fix this by assigning them to a local squad and then unassigning them.") + return + end + else + return + end if #squad.positions > unit.military.squad_position then return squad.positions[unit.military.squad_position] end @@ -97,7 +117,7 @@ local function process(unit, args) local to_drop = {} -- item id to item object -- First get squad position for an early-out for non-military dwarves - local squad_position = get_squad_position(unit) + local squad_position = get_squad_position(unit, unit_name) if not squad_position then if not silent then print("Unit " .. unit_name .. " does not have a military uniform.") From 41f1c4a398221a1b04a6239d50ede73ac6a81a47 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 6 Jan 2024 15:58:53 -0800 Subject: [PATCH 30/61] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 439ad2ec6..959d30902 100644 --- a/changelog.txt +++ b/changelog.txt @@ -55,6 +55,7 @@ Template for new versions: - `gui/autobutcher`: interface redesigned to better support mouse control - `gui/launcher`: now persists the most recent 32KB of command output even if you close it and bring it back up - `gui/quickcmd`: clickable buttons for command add/remove/edit operations +- `uniform-unstick`: warn if a unit belongs to a squad from a different site (can happen with migrants from previous forts) - `gui/mass-remove`: can now differentiate planned constructions, stockpiles, and regular buildings - `gui/mass-remove`: can now remove zones From cf9373487d8247bcaf21099412490744e09f729e Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 6 Jan 2024 17:34:26 -0800 Subject: [PATCH 31/61] allow gui/sandbox to blink when force pause is tested --- gui/sandbox.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/gui/sandbox.lua b/gui/sandbox.lua index 3866fd309..801623ed6 100644 --- a/gui/sandbox.lua +++ b/gui/sandbox.lua @@ -258,7 +258,6 @@ SandboxScreen = defclass(SandboxScreen, gui.ZScreen) SandboxScreen.ATTRS { focus_path='sandbox', force_pause=true, - pass_pause=false, defocusable=false, } From 6a65e04f4f9dc745f107cc89d8e06e56c6ac4d34 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 6 Jan 2024 23:31:20 -0800 Subject: [PATCH 32/61] use new divider widget in gui/launcher --- gui/launcher.lua | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/gui/launcher.lua b/gui/launcher.lua index 79a4c55b9..5752c2115 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -579,6 +579,8 @@ function LauncherUI:init(args) on_edit_input=self:callback('on_edit_input'), } + local function not_minimized() return not self.minimal end + local frame_r = get_frame_r() local update_frames = function() @@ -621,7 +623,7 @@ function LauncherUI:init(args) on_autocomplete=self:callback('on_autocomplete'), on_double_click=function(_,c) self:run_command(true, c.text) end, on_double_click2=function(_,c) self:run_command(false, c.text) end, - visible=function() return not self.minimal end}, + visible=not_minimized}, EditPanel{ view_id='edit', frame={t=0, l=0}, @@ -637,7 +639,16 @@ function LauncherUI:init(args) HelpPanel{ view_id='help', frame={t=EDIT_PANEL_HEIGHT+1, l=0, r=AUTOCOMPLETE_PANEL_WIDTH+1}, - visible=function() return not self.minimal end}, + visible=not_minimized}, + widgets.Divider{ + frame={t=0, b=0, r=AUTOCOMPLETE_PANEL_WIDTH+1, w=1}, + frame_style_t=false, + frame_style_b=false, + visible=not_minimized}, + widgets.Divider{ + frame={t=EDIT_PANEL_HEIGHT, l=0, r=AUTOCOMPLETE_PANEL_WIDTH+1, h=1}, + frame_style_l=false, + visible=not_minimized}, } self:addviews{main_panel} From 6ceac51b5a6e29aa4ce0e62678d4a86c662c7fa8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 7 Jan 2024 00:23:36 -0800 Subject: [PATCH 33/61] mark the divider as interior --- gui/launcher.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/gui/launcher.lua b/gui/launcher.lua index 5752c2115..8b9364e1a 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -647,6 +647,7 @@ function LauncherUI:init(args) visible=not_minimized}, widgets.Divider{ frame={t=EDIT_PANEL_HEIGHT, l=0, r=AUTOCOMPLETE_PANEL_WIDTH+1, h=1}, + interior=true, frame_style_l=false, visible=not_minimized}, } From 2aa32e7e8a87b3f4f5791140c6e27e75cdea7b43 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 7 Jan 2024 00:30:43 -0800 Subject: [PATCH 34/61] remove unused code --- gui/launcher.lua | 38 -------------------------------------- 1 file changed, 38 deletions(-) diff --git a/gui/launcher.lua b/gui/launcher.lua index 8b9364e1a..bda87930b 100644 --- a/gui/launcher.lua +++ b/gui/launcher.lua @@ -488,44 +488,6 @@ function MainPanel:postUpdateLayout() config:write(self.frame) end -local H_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_thin, 6), ch=196, fg=COLOR_GREY, bg=COLOR_BLACK} -local V_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_thin, 5), ch=179, fg=COLOR_GREY, bg=COLOR_BLACK} -local TOP_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_window,2), ch=209, fg=COLOR_GREY, bg=COLOR_BLACK} -local BOTTOM_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_window,16), ch=207, fg=COLOR_GREY, bg=COLOR_BLACK} -local LEFT_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_window,8), ch=199, fg=COLOR_GREY, bg=COLOR_BLACK} -local RIGHT_SPLIT_PEN = dfhack.pen.parse{tile=curry(textures.tp_border_thin, 18), ch=180, fg=COLOR_GREY, bg=COLOR_BLACK} - --- paint autocomplete panel border -local function paint_vertical_border(rect) - local x = rect.x2 - (AUTOCOMPLETE_PANEL_WIDTH + 2) - local y1, y2 = rect.y1, rect.y2 - dfhack.screen.paintTile(TOP_SPLIT_PEN, x, y1) - dfhack.screen.paintTile(BOTTOM_SPLIT_PEN, x, y2) - for y=y1+1,y2-1 do - dfhack.screen.paintTile(V_SPLIT_PEN, x, y) - end -end - --- paint border between edit area and help area -local function paint_horizontal_border(rect) - local panel_height = EDIT_PANEL_HEIGHT + 1 - local x1, x2 = rect.x1, rect.x2 - local v_border_x = x2 - (AUTOCOMPLETE_PANEL_WIDTH + 2) - local y = rect.y1 + panel_height - dfhack.screen.paintTile(LEFT_SPLIT_PEN, x1, y) - dfhack.screen.paintTile(RIGHT_SPLIT_PEN, v_border_x, y) - for x=x1+1,v_border_x-1 do - dfhack.screen.paintTile(H_SPLIT_PEN, x, y) - end -end - -function MainPanel:onRenderFrame(dc, rect) - MainPanel.super.onRenderFrame(self, dc, rect) - if self.get_minimal() then return end - paint_vertical_border(rect) - paint_horizontal_border(rect) -end - function MainPanel:onInput(keys) if MainPanel.super.onInput(self, keys) then return true From f963104f68a14435eac15e4228d029b61bf71af3 Mon Sep 17 00:00:00 2001 From: Timur Kelman Date: Sun, 7 Jan 2024 12:23:21 +0100 Subject: [PATCH 35/61] adjust tileset file names --- gui/biomes.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/gui/biomes.lua b/gui/biomes.lua index e16a9f5fe..8627b7cc5 100644 --- a/gui/biomes.lua +++ b/gui/biomes.lua @@ -12,13 +12,13 @@ local INITIAL_INFO_HEIGHT = 15 local texturesOnOff8x12 = dfhack.textures.loadTileset('hack/data/art/on-off.png', 8, 12, true) local LIST_ITEM_HIGHLIGHTED = dfhack.textures.getTexposByHandle(texturesOnOff8x12[1]) -- yellow-ish indicator -local texturesOnOff = dfhack.textures.loadTileset('hack/data/art/on-off - top-left.png', 32, 32, true) +local texturesOnOff = dfhack.textures.loadTileset('hack/data/art/on-off_top-left.png', 32, 32, true) local TILE_HIGHLIGHTED = dfhack.textures.getTexposByHandle(texturesOnOff[1]) -- yellow-ish indicator if TILE_HIGHLIGHTED < 0 then -- use a fallback TILE_HIGHLIGHTED = 88 -- `X` end -local texturesSmallLetters = dfhack.textures.loadTileset('hack/data/art/curses_small_letters - top-left.png', 32, 32, true) +local texturesSmallLetters = dfhack.textures.loadTileset('hack/data/art/curses-small-letters_top-left.png', 32, 32, true) local TILE_STARTING_SYMBOL = dfhack.textures.getTexposByHandle(texturesSmallLetters[1]) if TILE_STARTING_SYMBOL < 0 then -- use a fallback TILE_STARTING_SYMBOL = 97 -- `a` From 327468efae27f045383f765d96b7a383bc73f7c0 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Wed, 10 Jan 2024 23:30:47 -0800 Subject: [PATCH 36/61] migrate scripts to new persistence API --- autofish.lua | 13 ++-- control-panel.lua | 9 ++- devel/prepare-save.lua | 21 +------ emigration.lua | 9 +-- fix/protect-nicks.lua | 9 +-- hermit.lua | 8 +-- internal/control-panel/common.lua | 3 +- once-per-save.lua | 100 ++++++++---------------------- prioritize.lua | 13 ++-- source.lua | 9 +-- starvingdead.lua | 8 +-- suspendmanager.lua | 10 ++- test/prioritize.lua | 2 - warn-stranded.lua | 32 +++------- 14 files changed, 73 insertions(+), 173 deletions(-) diff --git a/autofish.lua b/autofish.lua index 8ceaeb364..6d270c0b8 100644 --- a/autofish.lua +++ b/autofish.lua @@ -4,8 +4,6 @@ --@ enable=true --@ module=true -local json = require("json") -local persist = require("persist-table") local argparse = require("argparse") local repeatutil = require("repeat-util") @@ -26,16 +24,19 @@ end --- Save the current state of the script local function persist_state() - persist.GlobalTable[GLOBAL_KEY] = json.encode({enabled=enabled, - s_maxFish=s_maxFish, s_minFish=s_minFish, s_useRaw=s_useRaw, - isFishing=isFishing + dfhack.persistent.saveSiteData(GLOBAL_KEY, { + enabled=enabled, + s_maxFish=s_maxFish, + s_minFish=s_minFish, + s_useRaw=s_useRaw, + isFishing=isFishing, }) end --- Load the saved state of the script local function load_state() -- load persistent data - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or "") or {} + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) enabled = persisted_data.enabled or false s_maxFish = persisted_data.s_maxFish or 100 s_minFish = persisted_data.s_minFish or 75 diff --git a/control-panel.lua b/control-panel.lua index ba1ea8e8e..975000316 100644 --- a/control-panel.lua +++ b/control-panel.lua @@ -2,8 +2,6 @@ local argparse = require('argparse') local common = reqscript('internal/control-panel/common') -local json = require('json') -local persist = require('persist-table') local registry = reqscript('internal/control-panel/registry') local utils = require('utils') @@ -36,11 +34,12 @@ local function apply_autostart_config() end local function apply_fort_loaded_config() - if not safe_index(json.decode(persist.GlobalTable[GLOBAL_KEY] or ''), 'autostart_done') then + local state = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + if not state.autostart_done then apply_autostart_config() - persist.GlobalTable[GLOBAL_KEY] = json.encode({autostart_done=true}) + dfhack.persistent.saveSiteData(GLOBAL_KEY, {autostart_done=true}) end - local enabled_repeats = json.decode(persist.GlobalTable[common.REPEATS_GLOBAL_KEY] or '') or {} + local enabled_repeats = dfhack.persistent.getSiteData(common.REPEATS_GLOBAL_KEY, {}) for _, data in ipairs(registry.COMMANDS_BY_IDX) do if data.mode == 'repeat' and enabled_repeats[data.command] then common.apply_command(data) diff --git a/devel/prepare-save.lua b/devel/prepare-save.lua index c5add069d..20be678d7 100644 --- a/devel/prepare-save.lua +++ b/devel/prepare-save.lua @@ -1,19 +1,4 @@ -- Prepare the current save for devel/find-offsets ---[====[ - -devel/prepare-save -================== - -.. warning:: - - THIS SCRIPT IS STRICTLY FOR DFHACK DEVELOPERS. - -This script prepares the current savegame to be used -with `devel/find-offsets`. It CHANGES THE GAME STATE -to predefined values, and initiates an immediate -`quicksave`, thus PERMANENTLY MODIFYING the save. - -]====] local utils = require 'utils' @@ -89,11 +74,7 @@ local yearstr = df.global.cur_year..','..df.global.cur_year_tick print('Cur year and tick: '..yearstr) -dfhack.persistent.save{ - key='prepare-save/cur_year', - value=yearstr, - ints={df.global.cur_year, df.global.cur_year_tick} -} +dfhack.persistent.saveSiteDataString('prepare-save/cur_year', yearstr) -- Save diff --git a/emigration.lua b/emigration.lua index 22dc4cee3..1e9ece499 100644 --- a/emigration.lua +++ b/emigration.lua @@ -4,9 +4,6 @@ --@module = true --@enable = true -local json = require('json') -local persist = require('persist-table') - local GLOBAL_KEY = 'emigration' -- used for state change hooks and persistence enabled = enabled or false @@ -16,7 +13,7 @@ function isEnabled() end local function persist_state() - persist.GlobalTable[GLOBAL_KEY] = json.encode({enabled=enabled}) + dfhack.persistent.saveSiteData(GLOBAL_KEY, {enabled=enabled}) end function desireToStay(unit,method,civ_id) @@ -215,8 +212,8 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) return end - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') - enabled = (persisted_data or {enabled=false})['enabled'] + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {enabled=false}) + enabled = persisted_data.enabled event_loop() end diff --git a/fix/protect-nicks.lua b/fix/protect-nicks.lua index f31b539b4..ed573c5b6 100644 --- a/fix/protect-nicks.lua +++ b/fix/protect-nicks.lua @@ -3,9 +3,6 @@ --@ enable = true --@ module = true -local json = require('json') -local persist = require('persist-table') - local GLOBAL_KEY = 'fix-protect-nicks' enabled = enabled or false @@ -15,7 +12,7 @@ function isEnabled() end local function persist_state() - persist.GlobalTable[GLOBAL_KEY] = json.encode({enabled=enabled}) + dfhack.persistent.saveSiteData(GLOBAL_KEY, {enabled=enabled}) end -- Reassign all the units nicknames with "dfhack.units.setNickname" @@ -42,8 +39,8 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) return end - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') - enabled = (persisted_data or {enabled=false})['enabled'] + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {enabled=false}) + enabled = persisted_data.enabled event_loop() end diff --git a/hermit.lua b/hermit.lua index 57f0b0899..649004427 100644 --- a/hermit.lua +++ b/hermit.lua @@ -3,8 +3,6 @@ --@ module=true local argparse = require('argparse') -local json = require('json') -local persist = require('persist-table') local repeatutil = require('repeat-util') local GLOBAL_KEY = 'hermit' @@ -22,12 +20,12 @@ function isEnabled() end local function persist_state() - persist.GlobalTable[GLOBAL_KEY] = json.encode{enabled=enabled} + dfhack.persistent.saveSiteData(GLOBAL_KEY, {enabled=enabled}) end local function load_state() - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') or {} - enabled = persisted_data.enabled or false + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {enabled=false}) + enabled = persisted_data.enabled end function event_loop() diff --git a/internal/control-panel/common.lua b/internal/control-panel/common.lua index a2aa7b86b..a2fefbe40 100644 --- a/internal/control-panel/common.lua +++ b/internal/control-panel/common.lua @@ -3,7 +3,6 @@ local helpdb = require('helpdb') local json = require('json') local migration = reqscript('internal/control-panel/migration') -local persist = require('persist-table') local registry = reqscript('internal/control-panel/registry') local repeatUtil = require('repeat-util') local utils = require('utils') @@ -117,7 +116,7 @@ local function persist_enabled_repeats() cp_repeats[name] = true end end - persist.GlobalTable[REPEATS_GLOBAL_KEY] = json.encode(cp_repeats) + dfhack.persistent.saveSiteData(REPEATS_GLOBAL_KEY, cp_repeats) end function apply_command(data, enabled_map, enabled) diff --git a/once-per-save.lua b/once-per-save.lua index 5cb91d247..d7e8c7959 100644 --- a/once-per-save.lua +++ b/once-per-save.lua @@ -1,90 +1,40 @@ --- runs dfhack commands unless ran already in this save +-- runs dfhack commands unless ran already in this save (world) -local HELP = [====[ +local argparse = require('argparse') -once-per-save -============= -Runs commands like `multicmd`, but only unless -not already ran once in current save. You may actually -want `on-new-fortress`. +local GLOBAL_KEY = 'once-per-save' -Only successfully ran commands are saved. +local opts = { + help=false, + rerun=false, + reset=false, +} -Parameters: +local positionals = argparse.processArgsGetopt({...}, { + {'h', 'help', handler=function() opts.help = true end}, + {nil, 'rerun', handler=function() opts.rerun = true end}, + {nil, 'reset', handler=function() opts.reset = true end}, +}) ---help display this help ---rerun commands ignore saved commands ---reset deletes saved commands - -]====] - -local STORAGEKEY_PREFIX = 'once-per-save' -local storagekey = STORAGEKEY_PREFIX .. ':' .. tostring(df.global.plotinfo.site_id) - -local args = {...} -local rerun = false - -local utils = require 'utils' -local arg_help = utils.invert{"?", "-?", "-help", "--help"} -local arg_rerun = utils.invert{"-rerun", "--rerun"} -local arg_reset = utils.invert{"-reset", "--reset"} -if arg_help[args[1]] then - print(HELP) +if opts.help or positionals[1] == 'help' then + print(dfhack.script_help()) return -elseif arg_rerun[args[1]] then - rerun = true - table.remove(args, 1) -elseif arg_reset[args[1]] then - while dfhack.persistent.delete(storagekey) do end - table.remove(args, 1) end -if #args == 0 then return end - -local age = df.global.plotinfo.fortress_age -local year = df.global.cur_year -local year_tick = df.global.cur_year_tick -local year_tick_advmode = df.global.cur_year_tick_advmode -local function is_later(a, b) - for i, v in ipairs(a) do - if v < b[i] then - return true - elseif v > b[i] then - return false - --else: v == b[i] so keep iterating - end - end - return false +if opts.reset then + dfhack.persistent.deleteWorldData(GLOBAL_KEY) end +if #positionals == 0 then return end -local once_run = {} -if not rerun then - local entries = dfhack.persistent.get_all(storagekey) or {} - for i, entry in ipairs(entries) do - local ints = entry.ints - if ints[1] > age - or age == 0 and is_later({ints[2], ints[3], ints[4]}, {year, year_tick, year_tick_advmode}) - then - print (dfhack.current_script_name() .. ': unretired fortress, deleting `' .. entry.value .. '`') - --printall_recurse(entry) -- debug - entry:delete() - else - once_run[entry.value]=entry - end - end -end +local state = dfhack.persistent.getWorldData(GLOBAL_KEY, {}) -local save = dfhack.persistent.save for cmd in table.concat(args, ' '):gmatch("%s*([^;]+);?%s*") do - if not once_run[cmd] then - local ok = dfhack.run_command(cmd) == 0 - if ok then - once_run[cmd] = save({key = storagekey, - value = cmd, - ints = { age, year, year_tick, year_tick_advmode }}, - true) - elseif rerun and once_run[cmd] then - once_run[cmd]:delete() + cmd = cmd:trim() + if not state[cmd] or opts.rerun then + if dfhack.run_command(cmd) == CR_OK then + state[cmd] = {already_run=true} end end end + +dfhack.persistent.saveWorldData(GLOBAL_KEY, state) diff --git a/prioritize.lua b/prioritize.lua index ce284dc1f..9dd146ac5 100644 --- a/prioritize.lua +++ b/prioritize.lua @@ -3,9 +3,7 @@ --@enable = true local argparse = require('argparse') -local json = require('json') local eventful = require('plugins.eventful') -local persist = require('persist-table') local utils = require('utils') local GLOBAL_KEY = 'prioritize' -- used for state change hooks and persistence @@ -54,7 +52,12 @@ function isEnabled() end local function persist_state() - persist.GlobalTable[GLOBAL_KEY] = json.encode(get_watched_job_matchers()) + local data_to_persist = {} + -- convert enum keys into strings so json doesn't get confused and think the map is a list + for k, v in pairs(get_watched_job_matchers()) do + data_to_persist[tostring(k)] = v + end + dfhack.persistent.saveSiteData(GLOBAL_KEY, data_to_persist) end local function make_matcher_map(keys) @@ -613,8 +616,8 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) if sc ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then return end - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') or {} - -- sometimes the keys come back as strings; fix that up + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) + -- convert the string keys back into enum values for k,v in pairs(persisted_data) do if type(k) == 'string' then persisted_data[tonumber(k)] = v diff --git a/source.lua b/source.lua index d23459c56..06450e967 100644 --- a/source.lua +++ b/source.lua @@ -1,15 +1,12 @@ --@ module = true local repeatUtil = require('repeat-util') -local json = require('json') -local persist = require('persist-table') local GLOBAL_KEY = 'source' -- used for state change hooks and persistence g_sources_list = g_sources_list or {} - local function persist_state(liquidSources) - persist.GlobalTable[GLOBAL_KEY] = json.encode(liquidSources) + dfhack.persistent.saveSiteData(GLOBAL_KEY, liquidSources) end local function formatPos(pos) @@ -179,9 +176,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) return end - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') or {} - - g_sources_list = persisted_data + g_sources_list = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) load_liquid_source(g_sources_list) end diff --git a/starvingdead.lua b/starvingdead.lua index 8bf523fa3..f1519c8c0 100644 --- a/starvingdead.lua +++ b/starvingdead.lua @@ -3,8 +3,6 @@ --@module = true local argparse = require('argparse') -local json = require('json') -local persist = require('persist-table') local GLOBAL_KEY = 'starvingdead' @@ -15,8 +13,8 @@ function isEnabled() end local function persist_state() - persist.GlobalTable[GLOBAL_KEY] = json.encode({ - enabled = starvingDeadInstance ~= nil, + 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 }) @@ -32,7 +30,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) return end - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '{}') + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) if persisted_data.enabled then starvingDeadInstance = StarvingDead{ diff --git a/suspendmanager.lua b/suspendmanager.lua index e4aec1618..9c010e2d3 100644 --- a/suspendmanager.lua +++ b/suspendmanager.lua @@ -2,8 +2,6 @@ --@module = true --@enable = true -local json = require('json') -local persist = require('persist-table') local argparse = require('argparse') local eventful = require('plugins.eventful') local utils = require('utils') @@ -112,7 +110,7 @@ function isKeptSuspended(job) end local function persist_state() - persist.GlobalTable[GLOBAL_KEY] = json.encode({ + dfhack.persistent.saveSiteData(GLOBAL_KEY, { enabled=enabled, prevent_blocking=Instance.preventBlocking, }) @@ -745,9 +743,9 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) return end - local persisted_data = json.decode(persist.GlobalTable[GLOBAL_KEY] or '') - enabled = (persisted_data or {enabled=false})['enabled'] - Instance.preventBlocking = (persisted_data or {prevent_blocking=true})['prevent_blocking'] + local persisted_data = dfhack.persistent.getSiteData(GLOBAL_KEY, {enabled=false, prevent_blocking=true}) + enabled = persisted_data.enabled + Instance.preventBlocking = persisted_data.prevent_blocking update_triggers() end diff --git a/test/prioritize.lua b/test/prioritize.lua index a7f47bc8f..ded0f4ab5 100644 --- a/test/prioritize.lua +++ b/test/prioritize.lua @@ -1,5 +1,4 @@ local eventful = require('plugins.eventful') -local persist = require('persist-table') local prioritize = reqscript('prioritize') local utils = require('utils') local p = prioritize.unit_test_hooks @@ -18,7 +17,6 @@ local function get_mock_reactions() return mock_reactions end local function test_wrapper(test_fn) mock.patch({{eventful, 'onUnload', mock_eventful_onUnload}, {eventful, 'onJobInitiated', mock_eventful_onJobInitiated}, - {persist, 'GlobalTable', {}}, {prioritize, 'print', mock_print}, {prioritize, 'get_watched_job_matchers', get_mock_watched_job_matchers}, diff --git a/warn-stranded.lua b/warn-stranded.lua index eb6da3c99..cd0998974 100644 --- a/warn-stranded.lua +++ b/warn-stranded.lua @@ -7,7 +7,9 @@ local gui = require 'gui' local widgets = require 'gui.widgets' local argparse = require 'argparse' local args = {...} -local scriptPrefix = 'warn-stranded' + +local GLOBAL_KEY = 'warn-stranded_v2' + ignoresCache = ignoresCache or {} -- =============================================== @@ -72,16 +74,7 @@ end -- will return an empty array if needed. Clears and adds entries to our cache. -- Returns the new global ignoresCache value local function loadIgnoredUnits() - local ignores = dfhack.persistent.get_all(scriptPrefix) - ignoresCache = {} - - if ignores == nil then return ignoresCache end - - for _, entry in ipairs(ignores) do - unit_id = entry.ints[1] - ignoresCache[unit_id] = entry - end - + ignoresCache = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) return ignoresCache end @@ -107,17 +100,10 @@ end -- and from the ignoresCache table. -- Returns true if the unit was already ignored, false if it wasn't. local function toggleUnitIgnore(unit, refresh) - local entry = unitIgnored(unit, refresh) - - if entry then - entry:delete() - ignoresCache[unit.id] = nil - return true - else - entry = dfhack.persistent.save({key = scriptPrefix, ints = {unit.id}}, true) - ignoresCache[unit.id] = entry - return false - end + local was_ignored = unitIgnored(unit, refresh) + ignoresCache[unit.id] = not was_ignored or nil + dfhack.persistent.saveSiteData(GLOBAL_KEY, ignoresCache) + return was_ignored end -- Does the usual GUI pattern when groups can be in a partial state @@ -469,7 +455,7 @@ end -- Load ignores list on save game load -- WARNING: This has to be above `dfhack_flags.module` or it will not work as intended on first game load -dfhack.onStateChange[scriptPrefix] = function(state_change) +dfhack.onStateChange[GLOBAL_KEY] = function(state_change) if state_change ~= SC_MAP_LOADED or df.global.gamemode ~= df.game_mode.DWARF then return end From 784f31b7ebd6986d7c6ddcd0b037f4b3ea1fab64 Mon Sep 17 00:00:00 2001 From: LightHardt Date: Thu, 11 Jan 2024 19:08:45 -0600 Subject: [PATCH 37/61] Change old data structures to new --- embark-skills.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/embark-skills.lua b/embark-skills.lua index 2dd803839..e07615a85 100644 --- a/embark-skills.lua +++ b/embark-skills.lua @@ -42,13 +42,13 @@ function adjust(dwarves, callback) end local scr = dfhack.gui.getCurViewscreen() --as:df.viewscreen_setupdwarfgamest -if dfhack.gui.getCurFocus() ~= 'setupdwarfgame' or scr.show_play_now == 1 then +if not df.viewscreen_setupdwarfgamest:is_instance(scr) then qerror('Must be called on the "Prepare carefully" screen') end local dwarf_info = scr.dwarf_info local dwarves = dwarf_info -local selected_dwarf = {[0] = scr.dwarf_info[scr.dwarf_cursor]} --as:df.setup_character_info[] +local selected_dwarf = {[0] = scr.dwarf_info[scr.selected_u]} --as:df.setup_character_info[] local args = {...} if args[1] == 'points' then @@ -58,20 +58,20 @@ if args[1] == 'points' then end if args[3] ~= 'all' then dwarves = selected_dwarf end adjust(dwarves, function(dwf) - dwf.skill_points_remaining = points + dwf.skill_picks_left = points end) elseif args[1] == 'max' then if args[2] ~= 'all' then dwarves = selected_dwarf end adjust(dwarves, function(dwf) - for skill, level in pairs(dwf.skills) do - dwf.skills[skill] = df.skill_rating.Proficient + for skill, level in pairs(dwf.skilllevel) do + dwf.skilllevel[skill] = df.skill_rating.Proficient end end) elseif args[1] == 'legendary' then if args[2] ~= 'all' then dwarves = selected_dwarf end adjust(dwarves, function(dwf) - for skill, level in pairs(dwf.skills) do - dwf.skills[skill] = df.skill_rating.Legendary5 + for skill, level in pairs(dwf.skilllevel) do + dwf.skilllevel[skill] = df.skill_rating.Legendary5 end end) else From ec48668d9af853ac929ce181126c1a5724d264c5 Mon Sep 17 00:00:00 2001 From: LightHardt <70411544+LightHardt@users.noreply.github.com> Date: Thu, 11 Jan 2024 22:12:33 -0600 Subject: [PATCH 38/61] Update embark-skills.rst Change tag from "unavailable" to "fort units" --- docs/embark-skills.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/embark-skills.rst b/docs/embark-skills.rst index cdf54aa24..7145ef61c 100644 --- a/docs/embark-skills.rst +++ b/docs/embark-skills.rst @@ -3,7 +3,7 @@ embark-skills .. dfhack-tool:: :summary: Adjust dwarves' skills when embarking. - :tags: unavailable + :tags: fort units When selecting starting skills for your dwarves on the embark screen, this tool can manipulate the skill values or adjust the number of points you have From b0837def5d706aa4a58cec1ccd40a80a05ef3abe Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 12 Jan 2024 13:46:21 -0800 Subject: [PATCH 39/61] no spurious output on world unload --- source.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/source.lua b/source.lua index 06450e967..315d94d4f 100644 --- a/source.lua +++ b/source.lua @@ -91,7 +91,6 @@ local function delete_liquid_source(pos) end local function clear_liquid_sources() - print("Clearing all Sources") for k, v in ipairs(g_sources_list) do delete_source_at(k) end @@ -178,7 +177,7 @@ dfhack.onStateChange[GLOBAL_KEY] = function(sc) g_sources_list = dfhack.persistent.getSiteData(GLOBAL_KEY, {}) - load_liquid_source(g_sources_list) + load_liquid_source() end if dfhack_flags.module then From 45d74733497820b77e03ef7eaea693843b1308d5 Mon Sep 17 00:00:00 2001 From: LightHardt <70411544+LightHardt@users.noreply.github.com> Date: Fri, 12 Jan 2024 19:21:58 -0600 Subject: [PATCH 40/61] Update docs/embark-skills.rst Co-authored-by: Myk --- docs/embark-skills.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/embark-skills.rst b/docs/embark-skills.rst index 7145ef61c..61f123f9b 100644 --- a/docs/embark-skills.rst +++ b/docs/embark-skills.rst @@ -3,7 +3,7 @@ embark-skills .. dfhack-tool:: :summary: Adjust dwarves' skills when embarking. - :tags: fort units + :tags: embark armok units When selecting starting skills for your dwarves on the embark screen, this tool can manipulate the skill values or adjust the number of points you have From 72ce4b361ff4935e497a7a3add0ca13ff1f18e21 Mon Sep 17 00:00:00 2001 From: LightHardt <70411544+LightHardt@users.noreply.github.com> Date: Fri, 12 Jan 2024 19:25:29 -0600 Subject: [PATCH 41/61] Update embark-skills.lua Co-authored-by: Myk --- embark-skills.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embark-skills.lua b/embark-skills.lua index e07615a85..9eac8afbf 100644 --- a/embark-skills.lua +++ b/embark-skills.lua @@ -42,7 +42,7 @@ function adjust(dwarves, callback) end local scr = dfhack.gui.getCurViewscreen() --as:df.viewscreen_setupdwarfgamest -if not df.viewscreen_setupdwarfgamest:is_instance(scr) then +if not dfhack.gui.matchFocusString('setupdwarfgame/Dwarves', scr) then qerror('Must be called on the "Prepare carefully" screen') end From 0ece7a68ad3e576615c787c1b40d7bcb4ad94057 Mon Sep 17 00:00:00 2001 From: LightHardt <70411544+LightHardt@users.noreply.github.com> Date: Fri, 12 Jan 2024 19:28:50 -0600 Subject: [PATCH 42/61] Update embark-skills.lua Update error message to specify dwarves tab --- embark-skills.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/embark-skills.lua b/embark-skills.lua index 9eac8afbf..a6ff59118 100644 --- a/embark-skills.lua +++ b/embark-skills.lua @@ -43,7 +43,7 @@ end local scr = dfhack.gui.getCurViewscreen() --as:df.viewscreen_setupdwarfgamest if not dfhack.gui.matchFocusString('setupdwarfgame/Dwarves', scr) then - qerror('Must be called on the "Prepare carefully" screen') + qerror('Must be called on the "Prepare carefully" screen, "Dwarves" tab') end local dwarf_info = scr.dwarf_info From 3e11a6a5973dab6d42be8c38c92c5b24c95e21a8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Fri, 12 Jan 2024 17:29:10 -0800 Subject: [PATCH 43/61] support unremove for buildings and constructions --- changelog.txt | 1 + gui/mass-remove.lua | 113 +++++++++++++++++++++++++++++++++----------- 2 files changed, 87 insertions(+), 27 deletions(-) diff --git a/changelog.txt b/changelog.txt index 9fd829a28..45dbda65d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -60,6 +60,7 @@ Template for new versions: - `uniform-unstick`: warn if a unit belongs to a squad from a different site (can happen with migrants from previous forts) - `gui/mass-remove`: can now differentiate planned constructions, stockpiles, and regular buildings - `gui/mass-remove`: can now remove zones +- `gui/mass-remove`: can now cancel removal for buildings and constructions ## Removed diff --git a/gui/mass-remove.lua b/gui/mass-remove.lua index ea58e14ca..63b281283 100644 --- a/gui/mass-remove.lua +++ b/gui/mass-remove.lua @@ -8,19 +8,39 @@ local widgets = require('gui.widgets') local function noop() end -local function remove_building(built, planned, bld) +local function get_first_job(bld) + if not bld then return end + if #bld.jobs ~= 1 then return end + return bld.jobs[0] +end + +local function process_building(built, planned, remove, bld) if (built and bld:getBuildStage() == bld:getMaxBuildStage()) or (planned and bld:getBuildStage() ~= bld:getMaxBuildStage()) then - dfhack.buildings.deconstruct(bld) + if remove then + dfhack.buildings.deconstruct(bld) + else + local job = get_first_job(bld) + if not job or job.job_type ~= df.job_type.DestroyBuilding then return end + dfhack.job.removeJob(job) + end end end -local function remove_construction(built, planned, pos, bld) +local function process_construction(built, planned, remove, grid, pos, bld) if planned and bld then - remove_building(false, true, bld) + process_building(false, true, remove, bld) elseif built and not bld then - dfhack.constructions.designateRemove(pos) + if remove then + dfhack.constructions.designateRemove(pos) + else + local tileFlags = dfhack.maps.getTileFlags(pos) + tileFlags.dig = df.tile_dig_designation.No + dfhack.maps.getTileBlock(pos).flags.designated = true + local job = safe_index(grid, pos.z, pos.y, pos.x) + if job then dfhack.job.removeJob(job) end + end end end @@ -56,10 +76,9 @@ function DimsPanel:init() widgets.WrappedLabel{ text_to_wrap=self:callback('get_action_text') }, - widgets.TooltipLabel{ - indent=1, - text={{text=self:callback('get_area_text')}}, - show_tooltip=self.get_mark_fn + widgets.Label{ + text={{gap=1, text=self:callback('get_area_text')}}, + text_pen=COLOR_GRAY, }, } end @@ -86,6 +105,10 @@ local function is_something_selected() dfhack.gui.getSelectedCivZone(true) end +local function not_is_something_selected() + return not is_something_selected() +end + -- -- MassRemove -- @@ -93,7 +116,7 @@ end MassRemove = defclass(MassRemove, widgets.Window) MassRemove.ATTRS{ frame_title='Mass Remove', - frame={w=47, h=18, r=2, t=18}, + frame={w=47, h=19, r=2, t=18}, resizable=true, resize_min={h=9}, autoarrange_subviews=true, @@ -110,11 +133,15 @@ function MassRemove:init() }, widgets.WrappedLabel{ text_to_wrap='Designate buildings, constructions, stockpiles, and/or zones for removal.', - visible=function() return not is_something_selected() end, + visible=function() return not_is_something_selected() and self.subviews.remove:getOptionValue() end, + }, + widgets.WrappedLabel{ + text_to_wrap='Designate buildings or constructions to cancel removal.', + visible=function() return not_is_something_selected() and not self.subviews.remove:getOptionValue() end, }, DimsPanel{ get_mark_fn=function() return self.mark end, - visible=function() return not is_something_selected() end, + visible=not_is_something_selected, }, widgets.CycleHotkeyLabel{ view_id='buildings', @@ -124,11 +151,12 @@ function MassRemove:init() option_gap=5, options={ {label='Leave alone', value=noop, pen=COLOR_BLUE}, - {label='Remove built and planned', value=curry(remove_building, true, true), pen=COLOR_RED}, - {label='Remove built', value=curry(remove_building, true, false), pen=COLOR_LIGHTRED}, - {label='Remove planned', value=curry(remove_building, false, true), pen=COLOR_YELLOW}, + {label='Affect built and planned', value=curry(process_building, true, true), pen=COLOR_RED}, + {label='Affect built', value=curry(process_building, true, false), pen=COLOR_LIGHTRED}, + {label='Affect planned', value=curry(process_building, false, true), pen=COLOR_YELLOW}, }, initial_option=2, + enabled=not_is_something_selected, }, widgets.CycleHotkeyLabel{ view_id='constructions', @@ -138,10 +166,11 @@ function MassRemove:init() option_gap=1, options={ {label='Leave alone', value=noop, pen=COLOR_BLUE}, - {label='Remove built and planned', value=curry(remove_construction, true, true), pen=COLOR_RED}, - {label='Remove built', value=curry(remove_construction, true, false), pen=COLOR_LIGHTRED}, - {label='Remove planned', value=curry(remove_construction, false, true), pen=COLOR_YELLOW}, + {label='Affect built and planned', value=curry(process_construction, true, true), pen=COLOR_RED}, + {label='Affect built', value=curry(process_construction, true, false), pen=COLOR_LIGHTRED}, + {label='Affect planned', value=curry(process_construction, false, true), pen=COLOR_YELLOW}, }, + enabled=not_is_something_selected, }, widgets.CycleHotkeyLabel{ view_id='stockpiles', @@ -153,6 +182,17 @@ function MassRemove:init() {label='Leave alone', value=noop, pen=COLOR_BLUE}, {label='Remove', value=remove_stockpile, pen=COLOR_RED}, }, + enabled=not_is_something_selected, + visible=function() return self.subviews.remove:getOptionValue() end, + }, + widgets.CycleHotkeyLabel{ + label='Stockpiles:', + key='CUSTOM_S', + key_sep=': ', + option_gap=4, + options={{label='Leave alone', value=noop}}, + enabled=false, + visible=function() return not self.subviews.remove:getOptionValue() end, }, widgets.CycleHotkeyLabel{ view_id='zones', @@ -164,6 +204,28 @@ function MassRemove:init() {label='Leave alone', value=noop, pen=COLOR_BLUE}, {label='Remove', value=remove_zone, pen=COLOR_RED}, }, + enabled=not_is_something_selected, + visible=function() return self.subviews.remove:getOptionValue() end, + }, + widgets.CycleHotkeyLabel{ + label='Zones:', + key='CUSTOM_Z', + key_sep=': ', + option_gap=9, + options={{label='Leave alone', value=noop}}, + enabled=false, + visible=function() return not self.subviews.remove:getOptionValue() end, + }, + widgets.CycleHotkeyLabel{ + view_id='remove', + label='Mode:', + key='CUSTOM_E', + options={ + {label='Remove or schedule for removal', value=true, pen=COLOR_RED}, + {label='Cancel removal', value=false, pen=COLOR_GREEN}, + }, + on_change=function() self:updateLayout() end, + enabled=not_is_something_selected, }, } @@ -250,12 +312,6 @@ local function is_destroying_construction(pos, grid) dfhack.maps.getTileFlags(pos).dig == df.tile_dig_designation.Default end -local function get_first_job(bld) - if not bld then return end - if #bld.jobs ~= 1 then return end - return bld.jobs[0] -end - local function get_job_pen(pos, grid) if is_destroying_construction(pos) then return DESTROYING_PEN @@ -297,6 +353,9 @@ function MassRemove:commit(bounds) local constr_fn = self.subviews.constructions:getOptionValue() local stockpile_fn = self.subviews.stockpiles:getOptionValue() local zones_fn = self.subviews.zones:getOptionValue() + local remove = self.subviews.remove:getOptionValue() + + self:refresh_grid() for z=bounds.z1,bounds.z2 do for y=bounds.y1,bounds.y2 do @@ -307,13 +366,13 @@ function MassRemove:commit(bounds) if bld:getType() == df.building_type.Stockpile then stockpile_fn(bld) elseif bld:getType() == df.building_type.Construction then - constr_fn(pos, bld) + constr_fn(remove, self.grid, pos, bld) else - bld_fn(bld) + bld_fn(remove, bld) end end if not dfhack.buildings.findAtTile(pos) and is_construction(pos) then - constr_fn(pos) + constr_fn(remove, self.grid, pos) end zones_fn(pos) end From c9aaed380b19f6d66bd2601484f2dccc9eb97fc2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 01:37:49 -0800 Subject: [PATCH 44/61] fix system_enable tools not showing correct status --- gui/control-panel.lua | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 9a3415760..c10b2d2f5 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -256,14 +256,15 @@ local function get_gui_config(command) end end -local function make_enabled_text(self, label, mode, gui_config) +local function make_enabled_text(self, command, mode, gui_config) + local label = command if mode == 'system_enable' then label = label .. ' (global)' end local function get_enabled_button_token(enabled_tile, disabled_tile) return { - tile=function() return self.enabled_map[label] and enabled_tile or disabled_tile end, + tile=function() return self.enabled_map[command] and enabled_tile or disabled_tile end, } end @@ -347,7 +348,7 @@ function EnabledTab:on_submit() _,choice = self.subviews.list:getSelected() if not choice then return end local data = choice.data - common.apply_command(data, self.enabled_map, not self.enabled_map[choice.data.command]) + common.apply_command(data, self.enabled_map, not self.enabled_map[data.command]) self:refresh() end From 1275bfbdc8cb582fd5ba59785ba2749275f68a12 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 02:08:08 -0800 Subject: [PATCH 45/61] narrow scope where remove zone is active --- internal/confirm/specs.lua | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index f8db58e80..81107f29c 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -432,7 +432,8 @@ ConfirmSpec{ title='Remove zone', message='Are you sure you want to remove this zone?', intercept_keys='_MOUSE_L', - context='dwarfmode/Zone', + context='dwarfmode/Zone', -- this is just Zone and not Zone/Some so we can pause across zones + predicate=function() print('predicate') return dfhack.gui.matchFocusString('dwarfmode/Zone/Some') end, intercept_frame={l=40, t=8, w=4, h=3}, pausable=true, } From 7ddb761227ee990981f573817f7a4ca8d9259364 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 04:41:44 -0800 Subject: [PATCH 46/61] remove debug print --- internal/confirm/specs.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 81107f29c..55d405185 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -433,7 +433,7 @@ ConfirmSpec{ message='Are you sure you want to remove this zone?', intercept_keys='_MOUSE_L', context='dwarfmode/Zone', -- this is just Zone and not Zone/Some so we can pause across zones - predicate=function() print('predicate') return dfhack.gui.matchFocusString('dwarfmode/Zone/Some') end, + predicate=function() return dfhack.gui.matchFocusString('dwarfmode/Zone/Some') end, intercept_frame={l=40, t=8, w=4, h=3}, pausable=true, } From e5abc41d6f65d3587538ec16052af1bc83b5ffd9 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 04:45:49 -0800 Subject: [PATCH 47/61] also autostart repeat commands --- control-panel.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/control-panel.lua b/control-panel.lua index 975000316..eea72bc5b 100644 --- a/control-panel.lua +++ b/control-panel.lua @@ -10,7 +10,7 @@ local GLOBAL_KEY = 'control-panel' -- state change hooks local function apply_system_config() - local enabled_map =common.get_enabled_map() + local enabled_map = common.get_enabled_map() for _, data in ipairs(registry.COMMANDS_BY_IDX) do if data.mode == 'system_enable' then common.apply_command(data, enabled_map) @@ -27,7 +27,7 @@ end local function apply_autostart_config() local enabled_map =common.get_enabled_map() for _, data in ipairs(registry.COMMANDS_BY_IDX) do - if data.mode == 'enable' or data.mode == 'run' then + if data.mode == 'enable' or data.mode == 'run' or data.mode == 'repeat' then common.apply_command(data, enabled_map) end end From 0dc5562a34feb2660e8f9c61e5f8a962be9e98f1 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 04:54:59 -0800 Subject: [PATCH 48/61] ensure script can load on program start so the overlay can be initialized and be shown --- uniform-unstick.lua | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 8ba40d6b8..78e56d65d 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -40,9 +40,7 @@ local function get_site_id() end end -local site_id = get_site_id() - -local function get_squad_position(unit, unit_name) +local function get_squad_position(unit, unit_name, site_id) local squad = df.squad.find(unit.military.squad_id) if squad then if squad.entity_id ~= site_id then @@ -105,7 +103,7 @@ local function print_bad_labor(unit_name, labor_name) end -- Will figure out which items need to be moved to the floor, returns an item_id:item map -local function process(unit, args) +local function process(unit, site_id, args) local silent = args.all -- Don't print details if we're iterating through all dwarves local unit_name = dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) @@ -117,7 +115,7 @@ local function process(unit, args) local to_drop = {} -- item id to item object -- First get squad position for an early-out for non-military dwarves - local squad_position = get_squad_position(unit, unit_name) + local squad_position = get_squad_position(unit, unit_name, site_id) if not squad_position then if not silent then print("Unit " .. unit_name .. " does not have a military uniform.") @@ -264,14 +262,15 @@ local function main(args) return end + local site_id = get_site_id() if args.all then for _, unit in ipairs(dfhack.units.getCitizens(false)) do - do_drop(process(unit, args)) + do_drop(process(unit, site_id, args)) end else local unit = dfhack.gui.getSelectedUnit() if unit then - do_drop(process(unit, args)) + do_drop(process(unit, site_id, args)) else qerror("Please select a unit if not running with --all") end From 40bbc18cd9dfa394731fafa0910947c46c9f9a4d Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 13:59:44 -0800 Subject: [PATCH 49/61] recategorize work-now as a gameplay tool --- 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 491c4ade3..cfa56dc3e 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -46,7 +46,6 @@ COMMANDS_BY_IDX = { {command='seedwatch', group='automation', mode='enable'}, {command='suspendmanager', group='automation', mode='enable'}, {command='tailor', group='automation', mode='enable'}, - {command='work-now', group='automation', mode='enable'}, -- bugfix tools {command='fix/blood-del', group='bugfix', mode='run', default=true}, @@ -88,6 +87,7 @@ COMMANDS_BY_IDX = { {command='warn-stranded', group='gameplay', mode='repeat', desc='Show a warning dialog when units are stranded from all others.', params={'--time', '2', '--timeUnits', 'days', '--command', '[', 'warn-stranded', ']'}}, + {command='work-now', group='gameplay', mode='enable'}, } COMMANDS_BY_NAME = {} From a0c46cfcc64f5998d132a89be1286995dc423bbe Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 14:44:09 -0800 Subject: [PATCH 50/61] use plotinfo to find the current site --- internal/caravan/common.lua | 2 +- internal/caravan/pedestal.lua | 2 +- internal/confirm/specs.lua | 4 ++-- internal/quickfort/zone.lua | 2 +- modtools/create-unit.lua | 11 ++++++----- uniform-unstick.lua | 20 ++++++++++---------- 6 files changed, 21 insertions(+), 20 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index dcd652365..3c85f3c04 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -388,7 +388,7 @@ end local function get_mandate_noble_roles() local roles = {} - for _, link in ipairs(df.global.world.world_data.active_site[0].entity_links) do + for _, link in ipairs(dfhack.getCurrentSite().entity_links) do local he = df.historical_entity.find(link.entity_id); if not he or (he.type ~= df.historical_entity_type.SiteGovernment and diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index 859e0f10e..497af51d3 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -105,7 +105,7 @@ local function get_containing_temple_or_guildhall(display_bld) end end if not loc_id then return end - local site = df.global.world.world_data.active_site[0] + local site = dfhack.getCurrentSite() local location = utils.binsearch(site.buildings, loc_id, 'id') if not location then return end local loc_type = location:getType() diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 55d405185..50f780646 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -87,8 +87,8 @@ local function has_caravans() end local function get_num_uniforms() - local site = df.global.world.world_data.active_site[0] - for _, entity_site_link in ipairs(site.entity_links) do + local site = dfhack.getCurrentSite() or {} + for _, entity_site_link in ipairs(site.entity_links or {}) do local he = df.historical_entity.find(entity_site_link.entity_id) if he and he.type == df.historical_entity_type.SiteGovernment then return #he.uniforms diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 4ca987ab1..78a17013f 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -312,7 +312,7 @@ local function set_location(zone, location, ctx) dfhack.printerr('cannot create a guildhall without a specified profession') return end - local site = df.global.world.world_data.active_site[0] + local site = dfhack.getCurrentSite() local loc_id = nil if location.label and safe_index(ctx, 'zone', 'locations', location.label) then local cached_loc = ctx.zone.locations[location.label] diff --git a/modtools/create-unit.lua b/modtools/create-unit.lua index 687be8ae1..0c90894fa 100644 --- a/modtools/create-unit.lua +++ b/modtools/create-unit.lua @@ -915,13 +915,14 @@ end function wildUnit(unit) local casteFlags = unit.enemy.caste_flags - -- x = df.global.world.world_data.active_site[0].pos.x - -- y = df.global.world.world_data.active_site[0].pos.y + -- x = dfhack.getCurrentSite().pos.x + -- y = dfhack.getCurrentSite().pos.y -- region = df.global.map.map_blocks[df.global.map.x_count_block*x+y] if not(casteFlags.CAN_SPEAK or casteFlags.CAN_LEARN) then - if #df.global.world.world_data.active_site > 0 then -- empty in adventure mode - unit.animal.population.region_x = df.global.world.world_data.active_site[0].pos.x - unit.animal.population.region_y = df.global.world.world_data.active_site[0].pos.y + if dfhack.isSiteLoaded() then + local site = dfhack.getCurrentSite() + unit.animal.population.region_x = site.pos.x + unit.animal.population.region_y = site.pos.y end unit.animal.population.unk_28 = -1 unit.animal.population.population_idx = -1 -- Eventually want to make a real population diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 78e56d65d..57fb110c7 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -30,9 +30,9 @@ local function get_item_pos(item) end end -local function get_site_id() - local fort = df.global.world.world_data.active_site[0] - for _, el in ipairs(fort.entity_links) do +local function get_gov_id() + local fort = dfhack.getCurrentSite() or {} + for _, el in ipairs(fort.entity_links or {}) do local he = df.historical_entity.find(el.entity_id) if he and he.type == df.historical_entity_type.SiteGovernment then return el.entity_id @@ -40,10 +40,10 @@ local function get_site_id() end end -local function get_squad_position(unit, unit_name, site_id) +local function get_squad_position(unit, unit_name, gov_id) local squad = df.squad.find(unit.military.squad_id) if squad then - if squad.entity_id ~= site_id then + if squad.entity_id ~= gov_id then print("WARNING: Unit " .. unit_name .. " is a member of a squad in another site!" .. " You can fix this by assigning them to a local squad and then unassigning them.") return @@ -103,7 +103,7 @@ local function print_bad_labor(unit_name, labor_name) end -- Will figure out which items need to be moved to the floor, returns an item_id:item map -local function process(unit, site_id, args) +local function process(unit, gov_id, args) local silent = args.all -- Don't print details if we're iterating through all dwarves local unit_name = dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) @@ -115,7 +115,7 @@ local function process(unit, site_id, args) 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, site_id) + local squad_position = get_squad_position(unit, unit_name, gov_id) if not squad_position then if not silent then print("Unit " .. unit_name .. " does not have a military uniform.") @@ -262,15 +262,15 @@ local function main(args) return end - local site_id = get_site_id() + local gov_id = get_gov_id() if args.all then for _, unit in ipairs(dfhack.units.getCitizens(false)) do - do_drop(process(unit, site_id, args)) + do_drop(process(unit, gov_id, args)) end else local unit = dfhack.gui.getSelectedUnit() if unit then - do_drop(process(unit, site_id, args)) + do_drop(process(unit, gov_id, args)) else qerror("Please select a unit if not running with --all") end From 54f69f3f0da61d205b631ad1e9dfb3bc1a73c418 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 14:50:57 -0800 Subject: [PATCH 51/61] use gov id from plotinfo instead of searching civ --- uniform-unstick.lua | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/uniform-unstick.lua b/uniform-unstick.lua index 57fb110c7..7dff267ae 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -30,20 +30,10 @@ local function get_item_pos(item) end end -local function get_gov_id() - local fort = dfhack.getCurrentSite() or {} - for _, el in ipairs(fort.entity_links or {}) do - local he = df.historical_entity.find(el.entity_id) - if he and he.type == df.historical_entity_type.SiteGovernment then - return el.entity_id - end - end -end - -local function get_squad_position(unit, unit_name, gov_id) +local function get_squad_position(unit, unit_name) local squad = df.squad.find(unit.military.squad_id) if squad then - if squad.entity_id ~= gov_id then + if squad.entity_id ~= df.global.plotinfo.group_id then print("WARNING: Unit " .. unit_name .. " is a member of a squad in another site!" .. " You can fix this by assigning them to a local squad and then unassigning them.") return @@ -103,7 +93,7 @@ local function print_bad_labor(unit_name, labor_name) end -- Will figure out which items need to be moved to the floor, returns an item_id:item map -local function process(unit, gov_id, args) +local function process(unit, args) local silent = args.all -- Don't print details if we're iterating through all dwarves local unit_name = dfhack.df2console(dfhack.TranslateName(dfhack.units.getVisibleName(unit))) @@ -115,7 +105,7 @@ local function process(unit, gov_id, args) 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, gov_id) + local squad_position = get_squad_position(unit, unit_name) if not squad_position then if not silent then print("Unit " .. unit_name .. " does not have a military uniform.") @@ -262,15 +252,14 @@ local function main(args) return end - local gov_id = get_gov_id() if args.all then for _, unit in ipairs(dfhack.units.getCitizens(false)) do - do_drop(process(unit, gov_id, args)) + do_drop(process(unit, args)) end else local unit = dfhack.gui.getSelectedUnit() if unit then - do_drop(process(unit, gov_id, args)) + do_drop(process(unit, args)) else qerror("Please select a unit if not running with --all") end From 34b2a8de13d80786c5dc1076cf7c36454089ff99 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 15:58:59 -0800 Subject: [PATCH 52/61] allow up/down stairs to be designated on top of up stairs --- internal/quickfort/dig.lua | 3 --- 1 file changed, 3 deletions(-) diff --git a/internal/quickfort/dig.lua b/internal/quickfort/dig.lua index 4aa7e33d3..1c76bac81 100644 --- a/internal/quickfort/dig.lua +++ b/internal/quickfort/dig.lua @@ -242,9 +242,6 @@ local function do_up_down_stair(digctx) return nil end end - if is_up_stair(digctx.tileattrs) then - return function() digctx.flags.dig = values.dig_downstair end - end return function() digctx.flags.dig = values.dig_updownstair end end From 53ea1f1f1d9222b748a05fe55cb07efa60b5a709 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 17:18:21 -0800 Subject: [PATCH 53/61] allow up/down designations to become down on floor --- internal/quickfort/dig.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/internal/quickfort/dig.lua b/internal/quickfort/dig.lua index 1c76bac81..b51d2cb25 100644 --- a/internal/quickfort/dig.lua +++ b/internal/quickfort/dig.lua @@ -238,10 +238,14 @@ local function do_up_down_stair(digctx) if is_construction(digctx.tileattrs) or (not is_wall(digctx.tileattrs) and not is_fortification(digctx.tileattrs) and + not is_diggable_floor(digctx.tileattrs) and not is_up_stair(digctx.tileattrs)) then return nil end end + if is_diggable_floor(digctx.tileattrs) then + return function() digctx.flags.dig = values.dig_downstair end + end return function() digctx.flags.dig = values.dig_updownstair end end From 342848d70fad30ae790138786d6241f698ff7c54 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 17:30:50 -0800 Subject: [PATCH 54/61] update changelog --- changelog.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/changelog.txt b/changelog.txt index 45dbda65d..684c5801e 100644 --- a/changelog.txt +++ b/changelog.txt @@ -42,6 +42,7 @@ Template for new versions: - `warn-stranded`: Update onZoom to use df's centering functionality - `ban-cooking`: fix banning creature alcohols resulting in error - `confirm`: properly detect clicks on the remove zone button even when the unit selection screen is also open (e.g. the vanilla assign animal to pasture panel) +- `quickfort`: if a blueprint specifies an up/down stair, but the tile the blueprint is applied to cannot make an up stair (e.g. it has already been dug out), still designate a down stair if possible ## Misc Improvements - `gui/control-panel`: reduce frequency for `warn-stranded` check to once every 2 days From f4de826ec0d2212b8b0d2339ee28f55a578cf93c Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 16:04:00 -0800 Subject: [PATCH 55/61] fix some commands not showing the correct help --- gui/control-panel.lua | 8 ++++---- internal/control-panel/registry.lua | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index c10b2d2f5..0b16c4a88 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -197,14 +197,14 @@ function CommandTab:init_footer(panel) } end -local function launch_help(command) - dfhack.run_command('gui/launcher', command .. ' ') +local function launch_help(data) + dfhack.run_command('gui/launcher', data.help_command or data.command .. ' ') end function CommandTab:show_help() _,choice = self.subviews.list:getSelected() if not choice then return end - launch_help(choice.data.command) + launch_help(choice.data) end function CommandTab:has_config() @@ -604,7 +604,7 @@ end function OverlaysTab:show_help() _,choice = self.subviews.list:getSelected() if not choice then return end - launch_help(choice.data.command) + launch_help(choice.data) end diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index cfa56dc3e..fd8bd3f20 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -22,11 +22,11 @@ COMMANDS_BY_IDX = { desc='Enable if you usually farm pig tails for the clothing industry.'}, {command='autofish', group='automation', mode='enable'}, --{command='autolabor', group='automation', mode='enable'}, -- hide until it works better - {command='automilk', group='automation', mode='repeat', + {command='automilk', help_command='workorder', group='automation', mode='repeat', desc='Automatically milk creatures that are ready for milking.', params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', '"{\\"job\\":\\"MilkCreature\\",\\"item_conditions\\":[{\\"condition\\":\\"AtLeast\\",\\"value\\":2,\\"flags\\":[\\"empty\\"],\\"item_type\\":\\"BUCKET\\"}]}"', ']'}}, {command='autonestbox', group='automation', mode='enable'}, - {command='autoshear', group='automation', mode='repeat', + {command='autoshear', help_command='workorder', group='automation', mode='repeat', desc='Automatically shear creatures that are ready for shearing.', params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', 'ShearCreature', ']'}}, {command='autoslab', group='automation', mode='enable'}, @@ -39,7 +39,7 @@ COMMANDS_BY_IDX = { desc='Encourage dwarves to drop tattered clothing and grab new ones.', params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, {command='nestboxes', group='automation', mode='enable'}, - {command='orders-sort', group='automation', mode='repeat', + {command='orders-sort', help_command='orders', group='automation', mode='repeat', desc='Sort manager orders by repeat frequency so one-time orders can be completed.', params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'orders', 'sort', ']'}}, {command='prioritize', group='automation', mode='enable'}, @@ -77,7 +77,7 @@ COMMANDS_BY_IDX = { {command='hide-tutorials', group='gameplay', mode='system_enable'}, {command='light-aquifers-only', group='gameplay', mode='run'}, {command='misery', group='gameplay', mode='enable'}, - {command='orders-reevaluate', group='gameplay', mode='repeat', + {command='orders-reevaluate', help_command='orders', group='gameplay', mode='repeat', desc='Invalidates all work orders once a month, allowing conditions to be rechecked.', params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'orders', 'recheck', ']'}}, {command='starvingdead', group='gameplay', mode='enable'}, From 93f2417686dec8f0ea1f9c6a33c7fa01e719e326 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 18:18:42 -0800 Subject: [PATCH 56/61] invert relationship between command tabs/subtabs --- gui/control-panel.lua | 442 +++++++++++++++++++++++------------------- 1 file changed, 243 insertions(+), 199 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 0b16c4a88..b92add9d4 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -119,135 +119,22 @@ function ConfigPanel:on_select(_, choice) end --- --- CommandTab --- -CommandTab = defclass(CommandTab, ConfigPanel) -local Subtabs = { - automation=1, - bugfix=2, - gameplay=3, -} -local Subtabs_revmap = utils.invert(Subtabs) -function CommandTab:init() - self.subpage = Subtabs.automation - self.blurbs = { - [Subtabs.automation]='These run in the background and help you manage your'.. - ' fort. They are always safe to enable, and allow you to concentrate on'.. - ' other aspects of gameplay that you find more enjoyable.', - [Subtabs.bugfix]='These automatically fix dangerous or annoying vanilla'.. - ' bugs. You should generally have all of these enabled.', - [Subtabs.gameplay]='These change or extend gameplay. Read their help docs to'.. - ' see what they do and enable the ones that appeal to you.', - } -end -function CommandTab:init_main_panel(panel) - panel:addviews{ - widgets.TabBar{ - frame={t=5}, - key='CUSTOM_CTRL_N', - key_back='CUSTOM_CTRL_M', - labels={ - 'Automation', - 'Bug Fixes', - 'Gameplay', - }, - on_select=function(val) - self.subpage = val - self:updateLayout() - self:refresh() - end, - get_cur_page=function() return self.subpage end, - }, - widgets.WrappedLabel{ - frame={t=7}, - text_to_wrap=function() return self.blurbs[self.subpage] end, - }, - widgets.FilteredList{ - frame={t=9}, - view_id='list', - on_select=self:callback('on_select'), - on_double_click=self:callback('on_submit'), - row_height=2, - }, - } -end -function CommandTab:init_footer(panel) - panel:addviews{ - widgets.HotkeyLabel{ - frame={t=0, l=0}, - label='Toggle enabled', - key='SELECT', - auto_width=true, - on_activate=self:callback('on_submit') - }, - widgets.HotkeyLabel{ - frame={t=1, l=0}, - label='Show full tool help or run custom command', - auto_width=true, - key='CUSTOM_CTRL_H', - on_activate=self:callback('show_help'), - }, - } -end -local function launch_help(data) - dfhack.run_command('gui/launcher', data.help_command or data.command .. ' ') -end -function CommandTab:show_help() - _,choice = self.subviews.list:getSelected() - if not choice then return end - launch_help(choice.data) -end -function CommandTab:has_config() - _,choice = self.subviews.list:getSelected() - return choice and choice.gui_config -end + -- --- EnabledTab +-- Enabled subtab functions -- -EnabledTab = defclass(EnabledTab, CommandTab) -EnabledTab.ATTRS{ - intro_text='These are the tools that can be enabled right now. Most tools can'.. - ' only be enabled when you have a fort loaded. Once enabled, tools'.. - ' will stay enabled when you save and reload your fort. If you want'.. - ' them to be auto-enabled for new forts, please see the "Autostart"'.. - ' tab.', -} - -function EnabledTab:init() - if not dfhack.world.isFortressMode() then - self.subpage = Subtabs.gameplay - end - - self.subviews.list.list.on_double_click2=self:callback('launch_config') -end - -function EnabledTab:init_footer(panel) - EnabledTab.super.init_footer(self, panel) - panel:addviews{ - widgets.HotkeyLabel{ - frame={t=2, l=26}, - label='Launch tool-specific config UI', - key='CUSTOM_CTRL_G', - auto_width=true, - enabled=self:callback('has_config'), - on_activate=self:callback('launch_config'), - }, - } -end - local function get_gui_config(command) command = common.get_first_word(command) local gui_config = 'gui/' .. command @@ -292,16 +179,15 @@ local function make_enabled_text(self, command, mode, gui_config) } end -function EnabledTab:refresh() +local function get_enabled_choices(self) local choices = {} self.enabled_map = common.get_enabled_map() - local group = Subtabs_revmap[self.subpage] for _,data in ipairs(registry.COMMANDS_BY_IDX) do if data.mode == 'run' then goto continue end if data.mode ~= 'system_enable' and not dfhack.world.isFortressMode() then goto continue end - if not common.command_passes_filters(data, group) then goto continue end + if not common.command_passes_filters(data, self.group) then goto continue end local gui_config = get_gui_config(data.command) table.insert(choices, { text=make_enabled_text(self, data.command, data.mode, gui_config), @@ -311,49 +197,30 @@ function EnabledTab:refresh() }) ::continue:: end - local list = self.subviews.list - local filter = list:getFilter() - local selected = list:getSelected() - list:setChoices(choices) - list:setFilter(filter, selected) - list.edit:setFocus(true) -end - -function EnabledTab:onInput(keys) - local handled = EnabledTab.super.onInput(self, keys) - if keys._MOUSE_L then - local list = self.subviews.list.list - local idx = list:getIdxUnderMouse() - if idx then - local x = list:getMousePos() - if x <= 2 then - self:on_submit() - elseif x >= 4 and x <= 6 then - self:show_help() - elseif x >= 8 and x <= 10 then - self:launch_config() - end + return choices +end + +local function enabled_onInput(self, keys) + if not keys._MOUSE_L then return end + local list = self.subviews.list.list + local idx = list:getIdxUnderMouse() + if idx then + local x = list:getMousePos() + if x <= 2 then + self:on_submit() + elseif x >= 4 and x <= 6 then + self:show_help() + elseif x >= 8 and x <= 10 then + self:launch_config() end end - return handled end -function EnabledTab:launch_config() - _,choice = self.subviews.list:getSelected() - if not choice or not choice.gui_config then return end - dfhack.run_command(choice.gui_config) -end - -function EnabledTab:on_submit() - _,choice = self.subviews.list:getSelected() - if not choice then return end - local data = choice.data +local function enabled_on_submit(self, data) common.apply_command(data, self.enabled_map, not self.enabled_map[data.command]) - self:refresh() end -function EnabledTab:restore_defaults() - local group = Subtabs_revmap[self.subpage] +local function enabled_restore_defaults(self) for _,data in ipairs(registry.COMMANDS_BY_IDX) do if data.mode == 'run' then goto continue end if (data.mode == 'enable' or data.mode == 'repeat') @@ -361,33 +228,17 @@ function EnabledTab:restore_defaults() then goto continue end - if not common.command_passes_filters(data, group) then goto continue end - common.apply_command(data, self.enabled_map, data.default) + if not common.command_passes_filters(data, self.group) then goto continue end + common.apply_command(data, self.enabled_map, not not data.default) ::continue:: end - self:refresh() - dialogs.showMessage('Success', - ('Enabled defaults restored for %s tools.'):format(group)) -end - --- pick up enablement changes made from other sources (e.g. gui config tools) -function EnabledTab:onRenderFrame(dc, rect) - self.enabled_map = common.get_enabled_map() - EnabledTab.super.onRenderFrame(self, dc, rect) end -- --- AutostartTab +-- Autostart subtab functions -- -AutostartTab = defclass(AutostartTab, CommandTab) -AutostartTab.ATTRS{ - intro_text='Tools that are enabled on this page will be auto-run or auto-enabled'.. - ' for you when you start a new fort (or, for "global" tools, when you start the game). To see tools that are enabled'.. - ' right now, please click on the "Enabled" tab.', -} - local function make_autostart_text(label, mode, enabled) if mode == 'system_enable' then label = label .. ' (global)' @@ -405,11 +256,10 @@ local function make_autostart_text(label, mode, enabled) } end -function AutostartTab:refresh() +local function get_autostart_choices(self) local choices = {} - local group = Subtabs_revmap[self.subpage] for _,data in ipairs(registry.COMMANDS_BY_IDX) do - if not common.command_passes_filters(data, group) then goto continue end + if not common.command_passes_filters(data, self.group) then goto continue end local enabled = safe_index(common.config.data.commands, data.command, 'autostart') if enabled == nil then enabled = data.default @@ -422,16 +272,10 @@ function AutostartTab:refresh() }) ::continue:: end - local list = self.subviews.list - local filter = list:getFilter() - local selected = list:getSelected() - list:setChoices(choices) - list:setFilter(filter, selected) - list.edit:setFocus(true) + return choices end -function AutostartTab:onInput(keys) - local handled = EnabledTab.super.onInput(self, keys) +local function autostart_onInput(self, keys) if keys._MOUSE_L then local list = self.subviews.list.list local idx = list:getIdxUnderMouse() @@ -444,33 +288,227 @@ function AutostartTab:onInput(keys) end end end - return handled end -function AutostartTab:on_submit() - _,choice = self.subviews.list:getSelected() - if not choice then return end - local data = choice.data +local function autostart_on_submit(self, data) common.set_autostart(data, not choice.enabled) common.config:write() - self:refresh() end -function AutostartTab:restore_defaults() - local group = Subtabs_revmap[self.subpage] +local function autostart_restore_defaults(self) for _,data in ipairs(registry.COMMANDS_BY_IDX) do - if not common.command_passes_filters(data, group) then goto continue end + if not common.command_passes_filters(data, self.group) then goto continue end print(data.command, data.default) common.set_autostart(data, data.default) ::continue:: end common.config:write() +end + + +-- +-- CommandTab +-- + +CommandTab = defclass(CommandTab, ConfigPanel) +CommandTab.ATTRS { + group=DEFAULT_NIL, +} + +local Subtabs = { + enabled=1, + autostart=2, +} + +local subtab = Subtabs.enabled + +function CommandTab:init() + self.blurbs = { + [Subtabs.enabled]='These are the tools that can be enabled right now.'.. + ' Most tools can only be enabled when you have a fort loaded.'.. + ' Once enabled, tools will stay enabled when you save and reload'.. + ' your fort. If you want them to be auto-enabled for new forts,'.. + ' please see the "Autostart" tab.', + [Subtabs.autostart]='Tools that are enabled on this page will be'.. + ' auto-run or auto-enabled for you when you start a new fort (or,'.. + ' for "global" tools, when you start the game). To see tools that'.. + ' are enabled right now, please click on the "Enabled" tab.', + } +end + +function CommandTab:init_main_panel(panel) + panel:addviews{ + widgets.TabBar{ + view_id='subtabbar', + frame={t=5}, + key='CUSTOM_CTRL_N', + key_back='CUSTOM_CTRL_M', + labels={ + 'Enabled', + 'Autostart', + }, + on_select=function(val) + subtab = val + self:updateLayout() + self:refresh() + end, + get_cur_page=function() return subtab end, + }, + widgets.WrappedLabel{ + frame={t=7}, + text_to_wrap=function() return self.blurbs[subtab] end, + }, + widgets.FilteredList{ + frame={t=9}, + view_id='list', + on_select=self:callback('on_select'), + on_double_click=self:callback('on_submit'), + on_double_click2=self:callback('launch_config'), + row_height=2, + }, + } +end + +function CommandTab:init_footer(panel) + panel:addviews{ + widgets.HotkeyLabel{ + frame={t=0, l=0}, + label='Toggle enabled', + key='SELECT', + auto_width=true, + on_activate=self:callback('on_submit') + }, + widgets.HotkeyLabel{ + frame={t=1, l=0}, + label='Show full tool help or run custom command', + auto_width=true, + key='CUSTOM_CTRL_H', + on_activate=self:callback('show_help'), + }, + widgets.HotkeyLabel{ + frame={t=2, l=26}, + label='Launch tool-specific config UI', + key='CUSTOM_CTRL_G', + auto_width=true, + enabled=self:callback('has_config'), + visible=function() return subtab == Subtabs.enabled end, + on_activate=self:callback('launch_config'), + }, + } +end + +local function launch_help(data) + dfhack.run_command('gui/launcher', data.help_command or data.command .. ' ') +end + +function CommandTab:show_help() + _,choice = self.subviews.list:getSelected() + if not choice then return end + launch_help(choice.data) +end + +function CommandTab:has_config() + _,choice = self.subviews.list:getSelected() + return choice and choice.gui_config +end + +function CommandTab:launch_config() + if subtab ~= Subtabs.enabled then return end + _,choice = self.subviews.list:getSelected() + if not choice or not choice.gui_config then return end + dfhack.run_command(choice.gui_config) +end + +function CommandTab:refresh() + local choices = subtab == Subtabs.enabled and + get_enabled_choices(self) or get_autostart_choices(self) + local list = self.subviews.list + local filter = list:getFilter() + local selected = list:getSelected() + list:setChoices(choices) + list:setFilter(filter, selected) + list.edit:setFocus(true) +end + +function CommandTab:on_submit() + _,choice = self.subviews.list:getSelected() + if not choice then return end + if subtab == Subtabs.enabled then + enabled_on_submit(self, choice.data) + else + autostart_on_submit(self, choice.data) + end + self:refresh() +end + +-- pick up enablement changes made from other sources (e.g. gui config tools) +function CommandTab:onRenderFrame(dc, rect) + if subtab == Subtabs.enabled then + self.enabled_map = common.get_enabled_map() + end + CommandTab.super.onRenderFrame(self, dc, rect) +end + +function CommandTab:onInput(keys) + local handled = CommandTab.super.onInput(self, keys) + if subtab == Subtabs.enabled then + enabled_onInput(self, keys) + else + autostart_onInput(self, keys) + end + return handled +end + +function CommandTab:restore_defaults() + if subtab == Subtabs.enabled then + enabled_restore_defaults(self) + else + autostart_restore_defaults(self) + end self:refresh() dialogs.showMessage('Success', - ('Autostart defaults restored for %s tools.'):format(group)) + ('%s defaults restored for %s tools.'):format( + self.subviews.subtabbar.labels[subtab], self.group)) end +-- +-- AutomationTab +-- + +AutomationTab = defclass(AutomationTab, CommandTab) +AutomationTab.ATTRS{ + intro_text='These run in the background and help you manage your'.. + ' fort. They are always safe to enable, and allow you to concentrate'.. + ' on other aspects of gameplay that you find more enjoyable.', + group='automation', +} + + +-- +-- BugFixesTab +-- + +BugFixesTab = defclass(BugFixesTab, CommandTab) +BugFixesTab.ATTRS{ + intro_text='These automatically fix dangerous or annoying vanilla'.. + ' bugs. You should generally have all of these enabled.', + group='bugfix', +} + + +-- +-- GameplayTab +-- + +GameplayTab = defclass(GameplayTab, CommandTab) +GameplayTab.ATTRS{ + intro_text='These change or extend gameplay. Read their help docs to'.. + ' see what they do and enable the ones that appeal to you.', + group='gameplay' +} + + -- -- OverlaysTab -- @@ -820,7 +858,7 @@ end ControlPanel = defclass(ControlPanel, widgets.Window) ControlPanel.ATTRS { frame_title='DFHack Control Panel', - frame={w=68, h=44}, + frame={w=74, h=44}, resizable=true, resize_min={h=39}, autoarrange_subviews=true, @@ -832,8 +870,9 @@ function ControlPanel:init() widgets.TabBar{ frame={t=0}, labels={ - 'Enabled', - 'Autostart', + 'Automation', + 'Bug Fixes', + 'Gameplay', 'UI Overlays', 'Preferences', }, @@ -844,14 +883,19 @@ function ControlPanel:init() view_id='pages', frame={t=5, l=0, b=0, r=0}, subviews={ - EnabledTab{}, - AutostartTab{}, + AutomationTab{}, + BugFixesTab{}, + GameplayTab{}, OverlaysTab{}, PreferencesTab{}, }, }, } + if not dfhack.world.isFortressMode() then + self.subviews.pages:setSelected(3) + end + self:refresh_page() end From 56f5d8ac0633fc901c45b73374e16ce907df9556 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 21:06:46 -0800 Subject: [PATCH 57/61] add confirmation prompt before resetting to defaults --- gui/control-panel.lua | 56 ++++++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 22 deletions(-) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index b92add9d4..bd2a3ad60 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -5,7 +5,6 @@ local helpdb = require('helpdb') local textures = require('gui.textures') local overlay = require('plugins.overlay') local registry = reqscript('internal/control-panel/registry') -local utils = require('utils') local widgets = require('gui.widgets') local function get_icon_pens() @@ -460,15 +459,20 @@ function CommandTab:onInput(keys) end function CommandTab:restore_defaults() - if subtab == Subtabs.enabled then - enabled_restore_defaults(self) - else - autostart_restore_defaults(self) - end - self:refresh() - dialogs.showMessage('Success', - ('%s defaults restored for %s tools.'):format( - self.subviews.subtabbar.labels[subtab], self.group)) + dialogs.showYesNoPrompt('Are you sure?', + ('Are you sure you want to restore %s\ndefaults for %s tools?'):format( + self.subviews.subtabbar.labels[subtab], self.group), + nil, function() + if subtab == Subtabs.enabled then + enabled_restore_defaults(self) + else + autostart_restore_defaults(self) + end + self:refresh() + dialogs.showMessage('Success', + ('%s defaults restored for %s tools.'):format( + self.subviews.subtabbar.labels[subtab], self.group)) + end) end @@ -631,12 +635,16 @@ function OverlaysTab:on_submit() end function OverlaysTab:restore_defaults() - local state = overlay.get_state() - for name, db_entry in pairs(state.db) do - enable_overlay(name, db_entry.widget.default_enabled) - end - self:refresh() - dialogs.showMessage('Success', 'Overlay defaults restored.') + dialogs.showYesNoPrompt('Are you sure?', + 'Are you sure you want to restore overlay defaults?', + nil, function() + local state = overlay.get_state() + for name, db_entry in pairs(state.db) do + enable_overlay(name, db_entry.widget.default_enabled) + end + self:refresh() + dialogs.showMessage('Success', 'Overlay defaults restored.') + end) end function OverlaysTab:show_help() @@ -842,12 +850,16 @@ function PreferencesTab:set_val(val) end function PreferencesTab:restore_defaults() - for _,data in ipairs(registry.PREFERENCES_BY_IDX) do - common.set_preference(data, data.default) - end - common.config:write() - self:refresh() - dialogs.showMessage('Success', 'Default preferences restored.') + dialogs.showYesNoPrompt('Are you sure?', + 'Are you sure you want to restore default preferences?', + nil, function() + for _,data in ipairs(registry.PREFERENCES_BY_IDX) do + common.set_preference(data, data.default) + end + common.config:write() + self:refresh() + dialogs.showMessage('Success', 'Default preferences restored.') + end) end From 9e6978a02bf58aee57e3cde292270aafe59a16b2 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 21:14:48 -0800 Subject: [PATCH 58/61] add message about empty list when not in fort mode --- gui/control-panel.lua | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/gui/control-panel.lua b/gui/control-panel.lua index bd2a3ad60..274a42209 100644 --- a/gui/control-panel.lua +++ b/gui/control-panel.lua @@ -364,6 +364,17 @@ function CommandTab:init_main_panel(panel) on_double_click=self:callback('on_submit'), on_double_click2=self:callback('launch_config'), row_height=2, + visible=function() return #self.subviews.list:getChoices() > 0 end, + }, + widgets.Label{ + frame={t=9, l=0}, + text={ + 'Please load a fort to see the fort-mode tools. Alternately,', NEWLINE, + 'please switch to the "Autostart" tab to configure which', NEWLINE, + 'tools should be run or enabled on embark.', + }, + text_pen=COLOR_LIGHTRED, + visible=function() return #self.subviews.list:getChoices() == 0 end, }, } end From 49114d1abe4e2bf4d4cd9424525640da793e44cf Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 21:41:04 -0800 Subject: [PATCH 59/61] more explanation of what is happening and better handling of gui/reveal hell --- docs/gui/reveal.rst | 7 ++++--- gui/reveal.lua | 25 ++++++++++++++++++++----- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/docs/gui/reveal.rst b/docs/gui/reveal.rst index a909d7119..caf450c4a 100644 --- a/docs/gui/reveal.rst +++ b/docs/gui/reveal.rst @@ -26,6 +26,7 @@ Examples ``gui/reveal`` Reveal all "normal" terrain, but keep areas with late-game surprises hidden. ``gui/reveal hell`` - Fully reveal adamantine spires, gemstone pillars, and the underworld. Note - that keeping these areas unrevealed when you exit `gui/reveal` will trigger - all the surprise events immediately. + Fully reveal adamantine spires, gemstone pillars, and the underworld. The + game cannot be unpaused with these features revealed, so the choice to keep + the map unrevealed when you close `gui/reveal` is disabled when this option + is specified. diff --git a/gui/reveal.lua b/gui/reveal.lua index 7d05c31a4..8df777745 100644 --- a/gui/reveal.lua +++ b/gui/reveal.lua @@ -1,8 +1,7 @@ --@ module = true +local dialogs = require('gui.dialogs') local gui = require('gui') -local guidm = require('gui.dwarfmode') -local utils = require('utils') local widgets = require('gui.widgets') -- @@ -12,24 +11,40 @@ local widgets = require('gui.widgets') Reveal = defclass(Reveal, widgets.Window) Reveal.ATTRS { frame_title='Reveal', - frame={w=29, h=10, r=2, t=18}, + frame={w=32, h=14, r=2, t=18}, autoarrange_subviews=true, autoarrange_gap=1, + hell=DEFAULT_NIL, } function Reveal:init() + if self.hell then + self.frame.h = 15 + end + self:addviews{ widgets.WrappedLabel{ text_to_wrap='The map is revealed. The game will be force paused until you close this window.', }, + widgets.WrappedLabel{ + text_to_wrap='Areas with event triggers are kept hidden to avoid spoilers.', + text_pen=COLOR_YELLOW, + visible=not self.hell, + }, + widgets.WrappedLabel{ + text_to_wrap='Areas with event triggers have been revealed. The map must be hidden again before unpausing.', + text_pen=COLOR_RED, + visible=self.hell, + }, widgets.ToggleHotkeyLabel{ view_id='unreveal', key='CUSTOM_SHIFT_R', - label='Unreveal on close', + label='Restore map on close:', options={ {label='Yes', value=true, pen=COLOR_GREEN}, {label='No', value=false, pen=COLOR_RED}, }, + enabled=not self.hell, }, } end @@ -53,7 +68,7 @@ function RevealScreen:init() end dfhack.run_command(command) - self:addviews{Reveal{}} + self:addviews{Reveal{hell=self.hell}} end function RevealScreen:onDismiss() From c62795f1518bf6c9900b985423283b583e23faf8 Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sat, 13 Jan 2024 22:00:48 -0800 Subject: [PATCH 60/61] rewrite docs for new layout --- docs/gui/control-panel.rst | 80 ++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 34 deletions(-) diff --git a/docs/gui/control-panel.rst b/docs/gui/control-panel.rst index 4f3b8608f..e3b1de92d 100644 --- a/docs/gui/control-panel.rst +++ b/docs/gui/control-panel.rst @@ -18,61 +18,73 @@ The tabs can also be navigated with the keyboard, with the :kbd:`Ctrl`:kbd:`T` and :kbd:`Ctrl`:kbd:`Y` hotkeys. These are the default hotkeys for navigating DFHack tab bars. -The "Enabled" tab ------------------ +The "Automation", "Bug Fixes", and "Gameplay" tabs +-------------------------------------------------- -The "Enabled" tab shows tools that you can enable right now. You can select the -tool name to see a short description at the bottom of the list. Hit +These three tabs provide access to the three main subcategories of DFHack tools. +In general, you'll probably want to start with only the "Bugfix" tools enabled. +As you become more comfortable with vanilla systems, and some of them start to +become less fun and more toilsome, you can enable more of the "Automation" +tools to manage them for you. Finally, you can examine the tools on the +"Gameplay" tab and enable whatever you think sounds like fun :). + +Under each of these tabs, there are two subtabs: "Enabled" and "Autostart". The +subtabs can be navigated with the keyboard, using the :kbd:`Ctrl`:kbd:`N` and +:kbd:`Ctrl`:kbd:`M` hotkeys. + +The "Enabled" subtab +~~~~~~~~~~~~~~~~~~~~ + +The "Enabled" tab allows you to toggle which tools are enabled right now. You +can select the tool in the list to see a short description at the bottom. Hit :kbd:`Enter`, double click on the tool name, or click on the toggle on the far left to enable or disable that tool. Note that before a fort is loaded, there will be very few tools listed here. +Come back when a fort is loaded to see much more. -Tools are split into three subcategories: ``automation``, ``bugfix``, and -``gameplay``. In general, you'll probably want to start with only the -``bugfix`` tools enabled. As you become more comfortable with vanilla systems, -and some of them start to become less fun and more toilsome, you can enable -more of the ``automation`` tools to manage them for you. Finally, you can -examine the tools on the ``gameplay`` tab and enable whatever you think sounds -like fun :). +Once tools are enabled, they will save their state with your fort and +automatically re-enable themselves when you load that same fort again. -The category subtabs can also be navigated with the keyboard, with the -:kbd:`Ctrl`:kbd:`N` and :kbd:`Ctrl`:kbd:`M` hotkeys. - -Once tools are enabled (possible after you've loaded a fort), they will save -their state with your fort and automatically re-enable themselves when you load -that same fort again. - -You can hit :kbd:`Ctrl`:kbd:`H` or click on the help icon to show the help page for the selected tool in `gui/launcher`. You can also use this as shortcut to +You can hit :kbd:`Ctrl`:kbd:`H` or click on the help icon to show the help page +for the selected tool in `gui/launcher`. You can also use this as shortcut to run custom commandline commands to configure that tool manually. If the tool has an associated GUI config screen, a gear icon will also appear next to the help -icon. Hit :kbd:`Ctrl`:kbd:`G`, click on the gear icon, or Shift-double click the tool name to launch the relevant configuration interface. +icon. Hit :kbd:`Ctrl`:kbd:`G`, click on the gear icon, or Shift-double click +the tool name to launch the relevant configuration interface. .. _dfhack-examples-guide: -The "Autostart" tab -------------------- +The "Autostart" subtab +~~~~~~~~~~~~~~~~~~~~~~ -This tab is organized similarly to the "Enabled" tab, but instead of tools you -can enable now, it shows the tools that you can configure DFHack to auto-enable -or auto-run when you start the game or a new fort. You'll recognize many tools -from the "Enabled" tab here, but there are also useful one-time commands that -you might want to run at the start of a fort, like -`ban-cooking all `. +This subtab is organized similarly to the "Enabled" subtab, but instead of +tools you can enable now, it shows the tools that you can configure DFHack to +auto-enable or auto-run when you start the game or a new fort. You'll recognize +many tools from the "Enabled" subtab here, but there are also useful one-time +commands that you might want to run at the start of a fort, like +`ban-cooking all ` or (if you have "mortal mode" disabled in the +"Preferences" tab) god-mode tools like `light-aquifers-only`. The "UI Overlays" tab --------------------- -The overlays tab allows you to easily see which overlays are enabled, lets you -toggle them on and off, and gives you links for the related help text (which is -normally added at the bottom of the help page for the tool that provides the -overlay). If you want to reposition any of the overlay widgets, hit -:kbd:`Ctrl`:kbd:`G` or click on the the hotkey hint to launch `gui/overlay`. +DFHack overlays add information and additional functionality to the vanilla DF +screens. For example, the popular DFHack `Building Planner ` is +an overlay named ``buildingplan.planner`` that appears when you are building +something. + +The "Overlays" tab allows you to easily see which overlays are enabled, gives +you a short description of what each one does, lets you toggle them on and off, +and gives you links for the related help text (which is normally added at the +bottom of the help page for the tool that provides the overlay). If you want to +reposition any of the overlay widgets, hit :kbd:`Ctrl`:kbd:`G` or click on the +the hotkey hint to launch `gui/overlay`. The "Preferences" tab --------------------- -The preferences tab allows you to change DFHack's internal settings and +The "Preferences" tab allows you to change DFHack's internal settings and defaults, like whether DFHack's "mortal mode" is enabled -- hiding the god-mode tools from the UI, whether DFHack tools pause the game when they come up, or how long you can take between clicks and still have it count as a double-click. From ce8887333e60246f666240013f6be14b4b75786a Mon Sep 17 00:00:00 2001 From: Myk Taylor Date: Sun, 14 Jan 2024 01:04:22 -0800 Subject: [PATCH 61/61] move getCurrentSite from dfhack to world --- internal/caravan/common.lua | 2 +- internal/caravan/pedestal.lua | 2 +- internal/confirm/specs.lua | 2 +- internal/quickfort/zone.lua | 2 +- modtools/create-unit.lua | 6 +++--- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/internal/caravan/common.lua b/internal/caravan/common.lua index 3c85f3c04..cd6c8f1c6 100644 --- a/internal/caravan/common.lua +++ b/internal/caravan/common.lua @@ -388,7 +388,7 @@ end local function get_mandate_noble_roles() local roles = {} - for _, link in ipairs(dfhack.getCurrentSite().entity_links) do + for _, link in ipairs(dfhack.world.getCurrentSite().entity_links) do local he = df.historical_entity.find(link.entity_id); if not he or (he.type ~= df.historical_entity_type.SiteGovernment and diff --git a/internal/caravan/pedestal.lua b/internal/caravan/pedestal.lua index 497af51d3..748e85249 100644 --- a/internal/caravan/pedestal.lua +++ b/internal/caravan/pedestal.lua @@ -105,7 +105,7 @@ local function get_containing_temple_or_guildhall(display_bld) end end if not loc_id then return end - local site = dfhack.getCurrentSite() + local site = dfhack.world.getCurrentSite() local location = utils.binsearch(site.buildings, loc_id, 'id') if not location then return end local loc_type = location:getType() diff --git a/internal/confirm/specs.lua b/internal/confirm/specs.lua index 50f780646..07e302099 100644 --- a/internal/confirm/specs.lua +++ b/internal/confirm/specs.lua @@ -87,7 +87,7 @@ local function has_caravans() end local function get_num_uniforms() - local site = dfhack.getCurrentSite() or {} + local site = dfhack.world.getCurrentSite() or {} for _, entity_site_link in ipairs(site.entity_links or {}) do local he = df.historical_entity.find(entity_site_link.entity_id) if he and he.type == df.historical_entity_type.SiteGovernment then diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 78a17013f..74db29355 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -312,7 +312,7 @@ local function set_location(zone, location, ctx) dfhack.printerr('cannot create a guildhall without a specified profession') return end - local site = dfhack.getCurrentSite() + local site = dfhack.world.getCurrentSite() local loc_id = nil if location.label and safe_index(ctx, 'zone', 'locations', location.label) then local cached_loc = ctx.zone.locations[location.label] diff --git a/modtools/create-unit.lua b/modtools/create-unit.lua index 0c90894fa..9dd3e35aa 100644 --- a/modtools/create-unit.lua +++ b/modtools/create-unit.lua @@ -915,12 +915,12 @@ end function wildUnit(unit) local casteFlags = unit.enemy.caste_flags - -- x = dfhack.getCurrentSite().pos.x - -- y = dfhack.getCurrentSite().pos.y + -- x = dfhack.world.getCurrentSite().pos.x + -- y = dfhack.world.getCurrentSite().pos.y -- region = df.global.map.map_blocks[df.global.map.x_count_block*x+y] if not(casteFlags.CAN_SPEAK or casteFlags.CAN_LEARN) then if dfhack.isSiteLoaded() then - local site = dfhack.getCurrentSite() + local site = dfhack.world.getCurrentSite() unit.animal.population.region_x = site.pos.x unit.animal.population.region_y = site.pos.y end