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/changelog.txt b/changelog.txt index 343f8b61c..1110fd8f2 100644 --- a/changelog.txt +++ b/changelog.txt @@ -30,6 +30,8 @@ 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 +- `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 @@ -41,6 +43,7 @@ Template for new versions: - `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) - `fix/workorder-detail-fix`: fix item types not being passed properly on some modified work order jobs +- `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 @@ -56,8 +59,10 @@ 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 +- `gui/mass-remove`: can now cancel removal for buildings and constructions ## Removed diff --git a/control-panel.lua b/control-panel.lua index ba1ea8e8e..eea72bc5b 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') @@ -12,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) @@ -29,18 +27,19 @@ 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 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/docs/embark-skills.rst b/docs/embark-skills.rst index cdf54aa24..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: unavailable + :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 diff --git a/docs/gui/biomes.rst b/docs/gui/biomes.rst new file mode 100644 index 000000000..550b0b30f --- /dev/null +++ b/docs/gui/biomes.rst @@ -0,0 +1,17 @@ +gui/biomes +========== + +.. dfhack-tool:: + :summary: Visualize and inspect biome regions on the map. + :tags: fort inspection map + +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. + + +Usage +----- + +:: + + gui/biomes 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. 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/docs/gui/teleport.rst b/docs/gui/teleport.rst index 82d15abeb..6222af4f8 100644 --- a/docs/gui/teleport.rst +++ b/docs/gui/teleport.rst @@ -2,11 +2,19 @@ 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. + +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/embark-skills.lua b/embark-skills.lua index 2dd803839..a6ff59118 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 - qerror('Must be called on the "Prepare carefully" screen') +if not dfhack.gui.matchFocusString('setupdwarfgame/Dwarves', scr) then + qerror('Must be called on the "Prepare carefully" screen, "Dwarves" tab') 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 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/gui/biomes.lua b/gui/biomes.lua new file mode 100644 index 000000000..8627b7cc5 --- /dev/null +++ b/gui/biomes.lua @@ -0,0 +1,431 @@ +-- Visualize and inspect biome regions on the map. + +local RELOAD = false -- set to true when actively working on this script + +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 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 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", + 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 regionBiomeMap = {} +local biomesMap = {} +local biomeList = {} +local function gatherBiomeInfo(z) + local maxX, maxY, maxZ = dfhack.maps.getTileSize() + maxX = maxX - 1; maxY = maxY - 1; maxZ = maxZ - 1 + + 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) + 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 + 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 + + ::continue:: + end + end + --end +end + +-- 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) + +-------------------------------------------------------------------------------- + +local TITLE = "Biomes" + +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}, + frame = { + w = 47, + h = INITIAL_LIST_HEIGHT + 2 + INITIAL_INFO_HEIGHT, + -- just under the minimap: + r = 2, + t = 18, + }, +} + +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() + local list = widgets.List{ + view_id = 'list', + 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 + on_select = self:callback('onSelectEntry'), + } + local tooltip_panel = widgets.Panel{ + view_id='tooltip_panel', + autoarrange_subviews=true, + frame = { b = 0, h = INITIAL_INFO_HEIGHT }, + 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 + +local PEN_ACTIVE_ICON = dfhack.pen.parse{tile=LIST_ITEM_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: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 + + self:ShowTooltip(option) +end + +function BiomeVisualizerLegend:UpdateChoices() + local choices = self.list:getChoices() or {} + for i = #choices + 1, #biomeList do + local biomeExt = biomeList[i] + table.insert(choices, { + 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, + }) + end + 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.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 + +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 + + 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 + + return text +end + +function BiomeVisualizerLegend:onMouseHoverEntry(idx, option) + self:ShowTooltip(option or self.SelectedOption) +end + +function BiomeVisualizerLegend:ShowTooltip(option) + local text = get_tooltip_text(option) + + local tooltip_panel = self.tooltip_panel + local lbl = tooltip_panel.subviews.label + + 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 +BiomeVisualizer = defclass(BiomeVisualizer, gui.ZScreen) +BiomeVisualizer.ATTRS{ + focus_path='BiomeVisualizer', + pass_movement_keys=true, +} + +function BiomeVisualizer:init() + local legend = BiomeVisualizerLegend{view_id = 'legend'} + self:addviews{legend} +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) + self.subviews.legend:UpdateChoices() + end + + local function get_overlay_pen(pos) + local self = self + local safe_index = safe_index + local biomes = biomesMap + + local N = safe_index(biomes, pos.z, pos.y, pos.x) + if not N then return end + + 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 +end + +function BiomeVisualizer:onDismiss() + view = nil +end + +if not dfhack.isMapLoaded() then + qerror('gui/biomes requires a map to be loaded') +end + +if RELOAD and view then + view:dismiss() + -- view is nil now +end + +view = view and view:raise() or BiomeVisualizer{}:show() diff --git a/gui/control-panel.lua b/gui/control-panel.lua index 9a3415760..274a42209 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() @@ -119,135 +118,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(command) - dfhack.run_command('gui/launcher', command .. ' ') -end -function CommandTab:show_help() - _,choice = self.subviews.list:getSelected() - if not choice then return end - launch_help(choice.data.command) -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 @@ -256,14 +142,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 @@ -291,16 +178,15 @@ local function make_enabled_text(self, label, 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), @@ -310,49 +196,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 - common.apply_command(data, self.enabled_map, not self.enabled_map[choice.data.command]) - self:refresh() +local function enabled_on_submit(self, data) + common.apply_command(data, self.enabled_map, not self.enabled_map[data.command]) 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') @@ -360,33 +227,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)' @@ -404,11 +255,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 @@ -421,16 +271,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() @@ -443,32 +287,242 @@ 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, + 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 + +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() - dialogs.showMessage('Success', - ('Autostart defaults restored for %s tools.'):format(group)) 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() + 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 + + +-- +-- 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 @@ -592,18 +646,22 @@ 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() _,choice = self.subviews.list:getSelected() if not choice then return end - launch_help(choice.data.command) + launch_help(choice.data) end @@ -803,12 +861,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 @@ -819,7 +881,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, @@ -831,8 +893,9 @@ function ControlPanel:init() widgets.TabBar{ frame={t=0}, labels={ - 'Enabled', - 'Autostart', + 'Automation', + 'Bug Fixes', + 'Gameplay', 'UI Overlays', 'Preferences', }, @@ -843,14 +906,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 diff --git a/gui/launcher.lua b/gui/launcher.lua index 79a4c55b9..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 @@ -579,6 +541,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 +585,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 +601,17 @@ 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}, + interior=true, + frame_style_l=false, + visible=not_minimized}, } self:addviews{main_panel} 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 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() 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, } diff --git a/gui/teleport.lua b/gui/teleport.lua index 0a29d32f0..8f9686da7 100644 --- a/gui/teleport.lua +++ b/gui/teleport.lua @@ -1,116 +1,410 @@ --- A front-end for the teleport script +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) -gui/teleport -============ +local indicator = df.global.game.main_interface.recenter_indicator_m -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(include, unit) + if not unit then return false end + 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 -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=45, h=28, r=2, t=18}, + resizable=true, + resize_min={h=20}, + 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:reset_double_click() -- sets self.last_map_click_ms and self.last_map_click_pos + + -- pre-add UI selected unit, if any + 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.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')}, + 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 units to mouse cursor', + key='CUSTOM_CTRL_T', + auto_width=true, + on_activate=self:callback('do_teleport'), + 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, 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, w=29}, + label='Include friendly units', + 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, w=29}, + label='Include hostile units ', + key='CUSTOM_SHIFT_H', + initial_option=saved_hostile, + on_change=function(val) saved_hostile = val 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=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='Deselect unit', + auto_width=true, + on_activate=self:callback('remove_unit'), + enabled=function() return #self.selected_units.list > 0 end, + }, + widgets.HotkeyLabel{ + 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', + }, + }, + } + } } - self.in_pick_pos = false + + self:refresh_choices() +end + +function Teleport:reset_double_click() + self.last_map_click_ms = 0 + self.last_map_click_pos = {} +end + +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 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: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 TeleportSidebar:back() - if self.in_pick_pos then - self.in_pick_pos = false - else - self:dismiss() +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 TeleportSidebar:zoom_unit() - df.global.cursor:assign(xyz2pos(pos2xyz(self.unit.pos))) - self:getViewport():centerOn(self.unit.pos):set() +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)' + 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, + unit=unit + }) + end + table.sort(choices, function(a, b) return a.text < b.text end) + self.subviews.list:setChoices(choices) end -function TeleportSidebar:next_unit() - self:sendInputToParent('UNITVIEW_NEXT') +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 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:get_include() + local include = {citizens=false, friendly=false, hostile=false} + if next(self.subviews) then + 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 - 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:get_help_text() + 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 help_text ~= self.prev_help_text then + self.prev_help_text = help_text + end + return help_text +end + +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 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_box(bounds) + if not bounds then return 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) + 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 same_xyz(indicator, pos) then return end + 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.selected_units.list)) + 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 + +----------------- +-- TeleportScreen +-- + +TeleportScreen = defclass(TeleportScreen, gui.ZScreen) +TeleportScreen.ATTRS { + focus_path='autodump', + pass_movement_keys=true, + pass_mouse_clicks=false, + force_pause=true, +} + +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() + indicator.x = -30000 + view = nil end -TeleportSidebar():show() +view = view and view:raise() or TeleportScreen{}:show() 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/caravan/common.lua b/internal/caravan/common.lua index dcd652365..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(df.global.world.world_data.active_site[0].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 859e0f10e..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 = df.global.world.world_data.active_site[0] + 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 f8db58e80..07e302099 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.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 return #he.uniforms @@ -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() return dfhack.gui.matchFocusString('dwarfmode/Zone/Some') end, intercept_frame={l=40, t=8, w=4, h=3}, pausable=true, } 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/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index aeae86b6c..c9b9d636a 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,14 +39,13 @@ 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'}, {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}, @@ -79,7 +78,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'}, @@ -89,6 +88,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 = {} diff --git a/internal/quickfort/dig.lua b/internal/quickfort/dig.lua index 4aa7e33d3..b51d2cb25 100644 --- a/internal/quickfort/dig.lua +++ b/internal/quickfort/dig.lua @@ -238,11 +238,12 @@ 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_up_stair(digctx.tileattrs) then + 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 diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 4ca987ab1..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 = df.global.world.world_data.active_site[0] + 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 687be8ae1..9dd3e35aa 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.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 #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.world.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/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..315d94d4f 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) @@ -94,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 @@ -179,11 +175,9 @@ 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) + load_liquid_source() end if dfhack_flags.module then 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/uniform-unstick.lua b/uniform-unstick.lua index aedd747e6..7dff267ae 100644 --- a/uniform-unstick.lua +++ b/uniform-unstick.lua @@ -30,9 +30,17 @@ local function get_item_pos(item) end end -local function get_squad_position(unit) +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 ~= 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 + end + else + return + end if #squad.positions > unit.military.squad_position then return squad.positions[unit.military.squad_position] end @@ -97,7 +105,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.") 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