diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2ca5c64fcc..208bd78ba6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: args: ['--fix=lf'] - id: trailing-whitespace - repo: https://github.com/python-jsonschema/check-jsonschema - rev: 0.29.1 + rev: 0.29.2 hooks: - id: check-github-workflows - repo: https://github.com/Lucas-C/pre-commit-hooks diff --git a/changelog.txt b/changelog.txt index 95d5589fd3..827586883d 100644 --- a/changelog.txt +++ b/changelog.txt @@ -31,23 +31,30 @@ Items will now spawn correctly, and will be of the creature type and creature ca # Future ## New Tools -- `embark-anyone`: allows you to embark as any civilisation, including dead, and non-dwarven ones -- `idle-crafting`: Allow dwarfs to automatically satisfy their need to craft objects. -- `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationsips +- `embark-anyone`: allows you to embark as any civilization, including dead and non-dwarven ones +- `idle-crafting`: allow dwarves to independently satisfy their need to craft objects +- `gui/family-affairs`: (reinstated) inspect or meddle with pregnancies, marriages, or lover relationships ## New Features - `caravan`: DFHack dialogs for trade screens (both ``Bring goods to depot`` and the ``Trade`` barter screen) can now filter by item origins (foreign vs. fort-made) and can filter bins by whether they have a mix of ethically acceptable and unacceptable items in them - `caravan`: If you have managed to select an item that is ethically unacceptable to the merchant, an "Ethics warning" badge will now appear next to the "Trade" button. Clicking on the badge will show you which items that you have selected are problematic. The dialog has a button that you can click to deselect the problematic items in the trade list. - `confirm`: If you have ethically unacceptable items selected for trade, the "Are you sure you want to trade" confirmation will warn you about them +- `quickfort`: ``#zone`` blueprints now integrated with `preserve-rooms` so you can create a zone and automatically assign it to a noble or administrative role +- `position`: option to copy cursor position to clipboard ## Fixes -- `timestream`: ensure child growth events (e.g. becoming an adult) are not skipped over -- `empty-bin`: ``--liquids`` option correctly emptying containers filled with LIQUID_MISC -- `gui/design`: Update Line & Freeform tools to not overcount tiles +- `timestream`: ensure child growth events (e.g. becoming an adult) are not skipped +- `empty-bin`: ``--liquids`` option now correctly empties containers filled with LIQUID_MISC (like lye) +- `gui/design`: don't overcount "affected tiles" for Line & Freeform drawing tools ## Misc Improvements -- `gui/sitemap`: show whether a unit is friendly, hostile, or wildlife +- `gui/sitemap`: show whether a unit is friendly, hostile, or wild - `gui/sitemap`: show whether a unit is caged +- `gui/control-panel`: include option for turning off dumping of old clothes for `tailor`, for players who have magma pit dumps and want to save old clothes from being dumped into the magma +- `position`: report current historical era (e.g., "Age of Myth") + +## Documentation +- `gui/embark-anywhere`: add information about how the game determines world tile pathability and instructions for bridging two landmasses ## Removed diff --git a/docs/gui/embark-anywhere.rst b/docs/gui/embark-anywhere.rst index 4583b04cd7..db5c06fc34 100644 --- a/docs/gui/embark-anywhere.rst +++ b/docs/gui/embark-anywhere.rst @@ -11,8 +11,13 @@ embark in an inaccessible location on top of a mountain range? Go for it! Want to try a brief existence in the middle of the ocean? Nobody can stop you! Want to tempt fate by embarking *inside of* a necromancer tower? !!FUN!! +If you are using this tool to create a fort that will bridge two disconnected +areas of land, see `So you want to bridge a gap?`_ below for tips and caveats. + Any and all consequences of embarking in strange locations are up to you to -handle (possibly with other `armok ` tools). +handle (possibly with other `armok ` tools). In particular, +embarking in inaccessible locations will prevent migrants, caravans, and +visitors from arriving. Usage ----- @@ -21,9 +26,37 @@ Usage gui/embark-anywhere -The command will only work when you are on the screen where you can choose your -embark site. The DFHack logo is not displayed on that screen since its default -position conflicts with the vanilla embark size selection panel. Remember that -you can bring up DFHack's `context menu ` with -:kbd:`Ctrl`:kbd:`Shift`:kbd:`C` or the -`in-game command launcher ` directly with the backtick key (\`). +The command will only work when you are on the screen where you can choose the +embark site for your fort. + +So you want to bridge a gap? +---------------------------- + +A popular use case for this tool is to create a fort (or a series of forts) that +bridges two disconnected landmasses so sites on the two landmasses can reach +each other (that is, they can send raiding parties and/or engage in trade). + +However, the way this works is not entirely intuitive. + +A single large embark is not necessarily going to functionally connect the two +shores so that armies can cross the gap. You could still choose to use this +approach to build a continuous constructed bridge in fort mode for later use as +an *adventurer* in adventure mode, but it will not be usable by the other +characters/armies in the world. + +The DF world map is divided into blocks of 16x16 tiles. When you are choosing +where to embark and you move the mouse so that your embark area "shadow" moves +over a little bit -- that's one "tile". An embark area can span block +boundaries, and there is no indication on the map where those boundaries are. + +The way DF determines world pathability is to check if the ground is continuous +**or** if the enclosing 16x16 block contains the upper left tile of a fort +embark area. + +In order for a connection to be formed for armies, one fort upper left corner +must exist in each 16x16 block that contains part of the gap. + +Therefore, the simplest solution for making a "bridge" that armies can use (but +walking adventurers cannot) is to make a 1x1 fort every 16 tiles across the +water gap, starting on land on one shore and finishing on land on the opposite +shore. That will ensure that every 16x16 block in the gap is covered by a fort. diff --git a/docs/gui/seedwatch.rst b/docs/gui/seedwatch.rst index 95ae306c20..6ee1723dee 100644 --- a/docs/gui/seedwatch.rst +++ b/docs/gui/seedwatch.rst @@ -5,11 +5,11 @@ gui/seedwatch :summary: Manages seed and plant cooking based on seed stock levels. :tags: fort auto plants -This is the configuration interface for the `seedwatch` plugin. You can configure -a target stock amount for each seed type. If the number of seeds of that type falls -below the target, then the plants and seeds of that type will be protected from -cookery. If the number rises above the target + 20, then cooking will be allowed -again. +This is the configuration interface for the `seedwatch` plugin. You can +configure a target stock amount for each seed type. If the number of seeds of +that type falls below the target, then the plants and seeds of that type will +be protected from cookery. If the number rises above the target + 20, then +cooking will be allowed again. Usage ----- diff --git a/docs/position.rst b/docs/position.rst index fada2ec54c..72ab716b5d 100644 --- a/docs/position.rst +++ b/docs/position.rst @@ -5,13 +5,30 @@ position :summary: Report cursor and mouse position, along with other info. :tags: fort inspection map -This tool reports the current date, clock time, month, and season. It also -reports the cursor position (or just the z-level if no cursor), window size, and -mouse location on the screen. +This tool reports the current date, clock time, month, season, and historical +era. It also reports the keyboard cursor position (or just the z-level if no +active cursor), window size, and mouse location on the screen. + +Can also be used to copy the current keyboard cursor position for later use. Usage ----- :: - position + position [--copy] + +Examples +-------- + +``position`` + Print various information. +``position -c`` + Copy cursor position to system clipboard. + +Options +------- + +``-c``, ``--copy`` + Copy current keyboard cursor position to the clipboard in format ``0,0,0`` + instead of reporting info. For convenience with other tools. diff --git a/fix/dry-buckets.lua b/fix/dry-buckets.lua index b49e5b161f..27834abb82 100644 --- a/fix/dry-buckets.lua +++ b/fix/dry-buckets.lua @@ -1,7 +1,15 @@ +local argparse = require("argparse") + +local quiet = false + local emptied = 0 local in_building = 0 local water_type = dfhack.matinfo.find('WATER').type +argparse.processArgsGetopt({...}, { + {'q', 'quiet', handler=function() quiet = true end}, +}) + for _,item in ipairs(df.global.world.items.other.IN_PLAY) do local container = dfhack.items.getContainer(item) if container @@ -19,7 +27,9 @@ for _,item in ipairs(df.global.world.items.other.IN_PLAY) do end end -print('Emptied '..emptied..' buckets.') -if emptied > 0 then - print(('Unclogged %d wells.'):format(in_building)) +if not quiet then + print('Emptied '..emptied..' buckets.') + if emptied > 0 then + print(('Unclogged %d wells.'):format(in_building)) + end end diff --git a/gui/quickfort.lua b/gui/quickfort.lua index 2742ca7741..eae4c701fd 100644 --- a/gui/quickfort.lua +++ b/gui/quickfort.lua @@ -63,7 +63,7 @@ function BlueprintDialog:init() text_pen=COLOR_GREY, }, widgets.ToggleHotkeyLabel{ - frame={t=0, l=12}, + frame={t=0, l=12, w=20}, key='CUSTOM_ALT_L', label='Library:', options=options, @@ -72,7 +72,7 @@ function BlueprintDialog:init() on_change=self:callback('update_setting', 'show_library') }, widgets.ToggleHotkeyLabel{ - frame={t=0, l=35}, + frame={t=0, l=35, w=19}, key='CUSTOM_ALT_H', label='Hidden:', options=options, diff --git a/gui/seedwatch.lua b/gui/seedwatch.lua index 612b42ee48..ad40e175b6 100644 --- a/gui/seedwatch.lua +++ b/gui/seedwatch.lua @@ -1,270 +1,290 @@ --- config ui for seedwatch - +local dlg = require('gui.dialogs') local gui = require('gui') -local widgets = require('gui.widgets') local plugin = require('plugins.seedwatch') +local widgets = require('gui.widgets') -local PROPERTIES_HEADER = ' Quantity Target ' -local REFRESH_MS = 10000 -local MAX_TARGET = 2147483647 --- --- SeedSettings --- -SeedSettings = defclass(SeedSettings, widgets.Window) -SeedSettings.ATTRS{ - frame={l=5, t=5, w=35, h=9}, +local CH_UP = string.char(30) +local CH_DN = string.char(31) + +Seedwatch = defclass(Seedwatch, widgets.Window) +Seedwatch.ATTRS{ + frame_title='Seedwatch', + frame={w=58, h=25}, + frame_inset={t=1}, + resizable=true, } -function SeedSettings:init() - self:addviews{ - widgets.Label{ - frame={t=0, l=0}, - text='Seed: ', - }, - widgets.Label{ - view_id='name', - frame={t=0, l=6}, - text_pen=COLOR_GREEN, - }, - widgets.Label{ - frame={t=1, l=0}, - text='Quantity: ', - }, - widgets.Label{ - view_id='quantity', - frame={t=1, l=10}, - text_pen=COLOR_GREEN, - }, - widgets.EditField{ - view_id='target', - frame={t=2, l=0}, - label_text='Target: ', - key='CUSTOM_CTRL_T', - on_char=function(ch) return ch:match('%d') end, - on_submit=self:callback('commit'), - }, - widgets.HotkeyLabel{ - frame={t=4, l=0}, - key='SELECT', - label='Apply', - on_activate=self:callback('commit'), - }, - } +local function sort_noop(a, b) + -- this function is used as a marker and never actually gets called + error('sort_noop should not be called') end -function SeedSettings:show(choice, on_commit) - self.data = choice.data - self.on_commit = on_commit - self.subviews.name:setText(self.data.name) - self.subviews.quantity:setText(tostring(self.data.quantity)) - self.subviews.target:setText(tostring(self.data.target)) - self.visible = true - self:setFocus(true) - self:updateLayout() +local function sort_by_name_desc(a, b) + return a.data.name < b.data.name end -function SeedSettings:hide() - self:setFocus(false) - self.visible = false +local function sort_by_name_asc(a, b) + return a.data.name > b.data.name end -function SeedSettings:commit() - local target = math.tointeger(self.subviews.target.text) or 0 - target = math.min(MAX_TARGET, math.max(0, target)) - - plugin.seedwatch_setTarget(self.data.id, target) - self:hide() - self.on_commit() +local function sort_by_quantity_desc(a, b) + if a.data.quantity == b.data.quantity then + return sort_by_name_desc(a, b) + end + return a.data.quantity > b.data.quantity end -function SeedSettings:onInput(keys) - if keys.LEAVESCREEN or keys._MOUSE_R then - self:hide() - return true +local function sort_by_quantity_asc(a, b) + if a.data.quantity == b.data.quantity then + return sort_by_name_desc(a, b) end - SeedSettings.super.onInput(self, keys) - return true + return a.data.quantity < b.data.quantity end --- --- Seedwatch --- -Seedwatch = defclass(Seedwatch, widgets.Window) -Seedwatch.ATTRS { - frame_title='Seedwatch', - frame={w=60, h=27}, - resizable=true, - resize_min={h=25}, -} - -function Seedwatch:init() - local minimal = false - local saved_frame = {w=50, h=6, r=2, t=18} - local saved_resize_min = {w=saved_frame.w, h=saved_frame.h} - local function toggle_minimal() - minimal = not minimal - local swap = self.frame - self.frame = saved_frame - saved_frame = swap - swap = self.resize_min - self.resize_min = saved_resize_min - saved_resize_min = swap - self:updateLayout() - self:refresh_data() - end - local function is_minimal() - return minimal +local function sort_by_target_desc(a, b) + if a.data.target == b.data.target then + return sort_by_name_desc(a, b) end - local function is_not_minimal() - return not minimal + return a.data.target > b.data.target +end + +local function sort_by_target_asc(a, b) + if a.data.target == b.data.target then + return sort_by_name_desc(a, b) end + return a.data.target < b.data.target +end +function Seedwatch:init() self:addviews{ - widgets.ToggleHotkeyLabel{ - view_id='enable_toggle', - frame={t=0, l=0, w=31}, - label='Seedwatch is', - key='CUSTOM_CTRL_E', - options={{value=true, label='Enabled', pen=COLOR_GREEN}, - {value=false, label='Disabled', pen=COLOR_RED}}, - on_change=function(val) plugin.setEnabled(val) end, - }, - widgets.EditField{ - view_id='all', - frame={t=1, l=0}, - label_text='Target for all: ', - key='CUSTOM_CTRL_A', - on_char=function(ch) return ch:match('%d') end, - on_submit=function(text) - local target = math.tointeger(text) - if not target or target == '' then - target = 0 - elseif target > MAX_TARGET then - target = MAX_TARGET - end - plugin.seedwatch_setTarget('all', target) - self.subviews.list:setFilter('') - self:refresh_data() - self:update_choices() - end, - visible=is_not_minimal, - text='30', + widgets.CycleHotkeyLabel{ + view_id='sort', + frame={l=1, t=0, w=31}, + label='Sort by:', + key='CUSTOM_SHIFT_S', + options={ + {label='Name'..CH_DN, value=sort_by_name_desc}, + {label='Name'..CH_UP, value=sort_by_name_asc}, + {label='Quantity'..CH_DN, value=sort_by_quantity_desc}, + {label='Quantity'..CH_UP, value=sort_by_quantity_asc}, + {label='Target'..CH_DN, value=sort_by_target_desc}, + {label='Target'..CH_UP, value=sort_by_target_asc}, + }, + initial_option=sort_by_name_desc, + on_change=self:callback('refresh', 'sort'), }, - - widgets.HotkeyLabel{ - frame={r=0, t=0, w=10}, - key='CUSTOM_ALT_M', - label=string.char(31)..string.char(30), - on_activate=toggle_minimal}, - widgets.Label{ - view_id='minimal_summary', - frame={t=1, l=0, h=1}, - auto_height=false, - visible=is_minimal, - }, - widgets.Label{ - frame={t=3, l=0}, - text='Seed', - auto_width=true, - visible=is_not_minimal, - }, - widgets.Label{ - frame={t=3, r=0}, - text=PROPERTIES_HEADER, - auto_width=true, - visible=is_not_minimal, - }, - widgets.FilteredList{ - view_id='list', - frame={t=5, l=0, r=0, b=3}, - on_submit=self:callback('configure_seed'), - visible=is_not_minimal, - edit_key = 'CUSTOM_S', + widgets.ToggleHotkeyLabel{ + view_id='hide_nostock', + frame={t=0, l=24, w=31}, + key='CUSTOM_CTRL_H', + label='Show only in stock:', + on_change=self:callback('refresh', 'sort'), }, - widgets.Label{ - view_id='summary', - frame={b=0, l=0}, - visible=is_not_minimal, + widgets.Panel{ + view_id='list_panel', + frame={t=2, l=0, r=0, b=4}, + frame_style=gui.FRAME_INTERIOR, + subviews={ + widgets.CycleHotkeyLabel{ + view_id='sort_name', + frame={t=0, l=0, w=5}, + options={ + {label='Name', value=sort_noop}, + {label='Name'..CH_DN, value=sort_by_name_desc}, + {label='Name'..CH_UP, value=sort_by_name_asc}, + }, + initial_option=sort_by_name_desc, + option_gap=0, + on_change=self:callback('refresh', 'sort_name'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_quantity', + frame={t=0, r=12, w=9}, + options={ + {label='Quantity', value=sort_noop}, + {label='Quantity'..CH_DN, value=sort_by_quantity_desc}, + {label='Quantity'..CH_UP, value=sort_by_quantity_asc}, + }, + option_gap=0, + on_change=self:callback('refresh', 'sort_quantity'), + }, + widgets.CycleHotkeyLabel{ + view_id='sort_target', + frame={t=0, r=3, w=7}, + options={ + {label='Target', value=sort_noop}, + {label='Target'..CH_DN, value=sort_by_target_desc}, + {label='Target'..CH_UP, value=sort_by_target_asc}, + }, + option_gap=0, + on_change=self:callback('refresh', 'sort_target'), + }, + widgets.Label{ + view_id='disabled_warning', + visible=function() return not plugin.isEnabled() end, + frame={t=3, h=1}, + auto_width=true, + text={"Please enable seedwatch to change settings"}, + text_pen=COLOR_YELLOW + }, + widgets.List{ + view_id='list', + frame={t=2, b=0}, + visible=plugin.isEnabled, + on_double_click=self:callback('prompt_for_new_target'), + }, + }, }, - SeedSettings{ - view_id='seed_settings', - visible=false, + widgets.Panel{ + view_id='footer', + frame={l=1, r=1, b=0, h=3}, + subviews={ + widgets.Label{ + frame={t=0, l=0}, + text={ + 'Double click on a row or hit ', + {text='Enter', pen=COLOR_LIGHTGREEN}, + ' to set the target.' + }, + }, + widgets.ToggleHotkeyLabel{ + view_id='enable_toggle', + frame={t=2, l=0, w=29}, + label='Seedwatch is', + key='CUSTOM_CTRL_E', + options={{value=true, label='Enabled', pen=COLOR_GREEN}, + {value=false, label='Disabled', pen=COLOR_RED}}, + on_change=function(val) + plugin.setEnabled(val) + self:refresh() + end, + }, + widgets.HotkeyLabel{ + frame={t=2, l=31}, + label='Set all targets', + key='CUSTOM_CTRL_A', + auto_width=true, + on_activate=self:callback('prompt_for_all_targets'), + }, + }, }, - } - - self:refresh_data() end -function Seedwatch:configure_seed(idx, choice) - self.subviews.seed_settings:show(choice, function() - self:refresh_data() - self:update_choices() - end) +function Seedwatch:render(dc) + self.subviews.enable_toggle:setOption(plugin.isEnabled()) + Seedwatch.super.render(self, dc) end -function Seedwatch:update_choices() - local list = self.subviews.list - local name_width = list.frame_body.width - #PROPERTIES_HEADER - local fmt = '%-'..tostring(name_width)..'s %10d %10d ' - local choices = {} - local prior_search=self.subviews.list.edit.text - for k, v in pairs(self.data.seeds) do - local text = (fmt):format(v.name:sub(1,name_width), v.quantity or 0, v.target or 0) - table.insert(choices, {text=text, data=v}) +function Seedwatch:onInput(keys) + if keys.SELECT then + self:prompt_for_new_target(self.subviews.list:getSelected()) end + return Seedwatch.super.onInput(self, keys) +end - self.subviews.list:setChoices(choices) - if prior_search then self.subviews.list:setFilter(prior_search) end - self.subviews.list:updateLayout() +function Seedwatch:postUpdateLayout() + self:refresh() end -function Seedwatch:refresh_data() - self.subviews.enable_toggle:setOption(plugin.isEnabled()) - local watch_map, seed_counts = plugin.seedwatch_getData() - self.data = {} - self.data.sum = 0 - self.data.seeds_qty = 0 - self.data.seeds_watched = 0 - self.data.seeds = {} - for k,v in pairs(seed_counts) do - local seed = {} - seed.id = df.global.world.raws.plants.all[k].id - seed.name = df.global.world.raws.plants.all[k].seed_singular - seed.quantity = v - seed.target = watch_map[k] or 0 - self.data.seeds[k] = seed - if self.data.seeds[k].target > 0 then - self.data.seeds_watched = self.data.seeds_watched + 1 - end - self.data.seeds_qty = self.data.seeds_qty + v +local SORT_WIDGETS = { + 'sort', + 'sort_name', + 'sort_quantity', + 'sort_target', +} + +local function make_row_text(name, quantity, target, row_width) + return { + {text=name, width=row_width-22, pad_char=' '}, + ' ', {text=quantity, width=7, rjustify=true, pad_char=' '}, + ' ', {text=target, width=7, rjustify=true, pad_char=' '}, + } +end + +local plants_all = df.global.world.raws.plants.all + +function Seedwatch:refresh(sort_widget, sort_fn) + sort_widget = sort_widget or 'sort' + sort_fn = sort_fn or self.subviews.sort:getOptionValue() + if sort_fn == sort_noop then + self.subviews[sort_widget]:cycle() + return end - if self.subviews.all.text == '' then - self.subviews.all:setText('0') + for _,widget_name in ipairs(SORT_WIDGETS) do + self.subviews[widget_name]:setOption(sort_fn) end - local summary_text = ('Seeds quantity: %d watched: %d\n'):format(tostring(self.data.seeds_qty),tostring(self.data.seeds_watched)) - self.subviews.summary:setText(summary_text) - local minimal_summary_text = summary_text - self.subviews.minimal_summary:setText(minimal_summary_text) - self.next_refresh_ms = dfhack.getTickCount() + REFRESH_MS + local watch_map, seed_counts = plugin.seedwatch_getData() + local hide_nostock = self.subviews.hide_nostock:getOptionValue() -end + local list = self.subviews.list + local row_width = list.frame_body.width + local choices = {} + for idx,target in pairs(watch_map) do + if hide_nostock and not seed_counts[idx] then goto continue end + local name = plants_all[idx].seed_singular + local quantity = seed_counts[idx] or 0 + table.insert(choices, { + text=make_row_text(name, quantity, target, row_width), + data={ + id=plants_all[idx].id, + name=name, + quantity=quantity, + target=target, + }, + }) + ::continue:: + end -function Seedwatch:postUpdateLayout() - self:update_choices() + table.sort(choices, self.subviews.sort:getOptionValue()) + local selected = list:getSelected() + list:setChoices(choices, selected) end --- refreshes data every 10 seconds or so -function Seedwatch:onRenderBody() - if self.next_refresh_ms <= dfhack.getTickCount() - and self.subviews.seed_settings.visible == false - and not self.subviews.all.focus - and not self.subviews.list.edit.focus then - self:refresh_data() - self:update_choices() +local function check_number(target, text) + if not target then + dlg.showMessage('Invalid Number', 'This is not a number: '..text..NEWLINE..'(for zero enter a 0)', COLOR_LIGHTRED) + return false end + if target < 0 then + dlg.showMessage('Invalid Number', 'Negative numbers make no sense!', COLOR_LIGHTRED) + return false + end + return true +end + +function Seedwatch:prompt_for_new_target(_, choice) + dlg.showInputPrompt( + 'Set target', + ('Enter desired target for %s:'):format(choice.data.name), + COLOR_WHITE, + tostring(choice.data.target), + function(text) + local target = tonumber(text) + if check_number(target, text) then + plugin.seedwatch_setTarget(choice.data.id, target) + self:refresh() + end + end + ) +end + +function Seedwatch:prompt_for_all_targets() + dlg.showInputPrompt( + 'Set all targets', + 'Enter desired target for all seed types', + COLOR_WHITE, + '', + function(text) + local target = tonumber(text) + if check_number(target, text) then + plugin.seedwatch_setTarget('all', target) + self:refresh() + end + end + ) end -- @@ -272,7 +292,7 @@ end -- SeedwatchScreen = defclass(SeedwatchScreen, gui.ZScreen) -SeedwatchScreen.ATTRS { +SeedwatchScreen.ATTRS{ focus_path='seedwatch', } @@ -284,8 +304,8 @@ function SeedwatchScreen:onDismiss() view = nil end -if not dfhack.isMapLoaded() then - qerror('seedwatch requires a map to be loaded') +if not dfhack.isMapLoaded() or not dfhack.world.isFortressMode() then + qerror('seedwatch requires a fort map to be loaded') end view = view and view:raise() or SeedwatchScreen{}:show() diff --git a/idle-crafting.lua b/idle-crafting.lua index 784222b7c8..4f74ae7249 100644 --- a/idle-crafting.lua +++ b/idle-crafting.lua @@ -134,7 +134,7 @@ end local function checkForWorkshop() if not next(allowed) then - print('no available workshops, disabling') + -- print('no available workshops, disabling') stop() end end @@ -191,10 +191,10 @@ local function processUnit(workshop, idx, unit_id) watched[idx][unit_id] = nil return false elseif not canAccessWorkshop(unit, workshop) then - dfhack.print('-') + -- dfhack.print('-') return false elseif not unitIsAvailable(unit) then - dfhack.print('.') + -- dfhack.print('.') return false end -- We have an available unit @@ -205,10 +205,9 @@ local function processUnit(workshop, idx, unit_id) if not success and workshop.profile.blocked_labors[BONE_CARVE] == false then success = makeBoneCraft(unit, workshop) end - local name = (dfhack.TranslateName(dfhack.units.getVisibleName(unit))) if success then -- Why is the encoding still wrong, even when using df2console? - print(' assigned ' .. dfhack.df2console(name)) + print('idle-crafting: assigned crafting job to ' .. dfhack.df2console(dfhack.units.getReadableName(unit))) watched[idx][unit_id] = nil allowed[workshop.id] = df.global.world.frame_counter else @@ -239,7 +238,7 @@ local function unit_loop() local workshop = locateWorkshop(workshop_id) -- workshop may have been destroyed, assigned a master, or does not allow crafting if not workshop or invalidProfile(workshop) then - print('workshop destroyed or has invalid profile') + -- print('workshop destroyed or has invalid profile') allowed[workshop_id] = nil --clearing during iteration is permitted goto next_workshop end @@ -251,14 +250,14 @@ local function unit_loop() -- check that we didn't schedule a job on the last iteration if (last_job_frame >= 0) and (current_frame < last_job_frame + 60) then - print(('idle-crafting: disabling failing workshop (%d) until the next run of main loop'): - format(workshop_id)) + -- print(('idle-crafting: disabling failing workshop (%d) until the next run of main loop'): + -- format(workshop_id)) failing[workshop_id] = true goto next_workshop end - dfhack.print(('idle-crafting: locating crafter for %s (%d)'): - format(dfhack.buildings.getName(workshop), workshop_id)) + -- dfhack.print(('idle-crafting: locating crafter for %s (%d)'): + -- format(dfhack.buildings.getName(workshop), workshop_id)) -- workshop is free to use, try to find a unit for idx, _ in ipairs(thresholds) do @@ -267,10 +266,10 @@ local function unit_loop() goto next_workshop end end - dfhack.print('/') + -- dfhack.print('/') end - print('no unit found') + -- print('no unit found') ::next_workshop:: end -- disable loop if there are no more units @@ -283,7 +282,7 @@ local function unit_loop() end local function main_loop() - print('idle crafting: running main loop') + -- print('idle crafting: running main loop') checkForWorkshop() if not enabled then return @@ -315,9 +314,9 @@ local function main_loop() end ::continue:: end - print(('watching %s dwarfs with crafting needs'):format( - table.concat(num_watched, '/') - )) + -- print(('watching %s dwarfs with crafting needs'):format( + -- table.concat(num_watched, '/') + -- )) if watching then repeatutil.scheduleUnlessAlreadyScheduled(GLOBAL_KEY .. 'unit', 53, 'ticks', unit_loop) @@ -361,23 +360,32 @@ IdleCraftingOverlay.ATTRS { viewscreens = { 'dwarfmode/ViewSheets/BUILDING/Workshop/Craftsdwarfs/Workers', }, - frame = { w = 55, h = 1 }, + frame = { w = 54, h = 1 }, } function IdleCraftingOverlay:init() self:addviews { - widgets.CycleHotkeyLabel { - view_id = 'leisure_toggle', - frame = { l = 0, t = 0 }, - label = 'Allow idle dwarves to satisfy crafting needs:', - key = 'CUSTOM_I', - options = { - { label = 'yes', value = true, pen = COLOR_GREEN }, - { label = 'no', value = false }, + widgets.BannerPanel{ + subviews={ + widgets.CycleHotkeyLabel { + view_id = 'leisure_toggle', + frame = { t=0, l = 1, r = 1 }, + label = 'Allow idle dwarves to satisfy crafting needs:', + key = 'CUSTOM_I', + options = { + { label = 'yes', value = true, pen = COLOR_GREEN }, + { label = 'no', value = false }, + }, + initial_option = 'no', + on_change = self:callback('onClick'), + enabled = function() + local bld = dfhack.gui.getSelectedBuilding(true) + if not bld then return end + return not invalidProfile(bld) + end, + } }, - initial_option = 'no', - on_change = self:callback('onClick'), - } + }, } end @@ -414,14 +422,12 @@ if dfhack_flags.module then end if df.global.gamemode ~= df.game_mode.DWARF then - print('this tool requires a loaded fort') - return + qerror('this tool requires a loaded fort') end if dfhack_flags.enable then if dfhack_flags.enable_state then - print('This tool is enabled by permitting idle crafting at a Craftsdarf\'s workshop') - return + qerror('This tool is enabled by permitting idle crafting at a Craftsdarf\'s workshop') else allowed = {} stop() diff --git a/internal/advtools/party.lua b/internal/advtools/party.lua index 241edc4782..4ef9f7cbf9 100644 --- a/internal/advtools/party.lua +++ b/internal/advtools/party.lua @@ -1,7 +1,7 @@ --@ module=true -local dialogs = require 'gui.dialogs' -local utils = require 'utils' +local dialogs = require('gui.dialogs') +local utils = require('utils') local makeown = reqscript('makeown') @@ -40,7 +40,7 @@ local function showExtraPartyPrompt() table.insert(choices, {text=name, nemesis=nemesis, search_key=dfhack.toSearchNormalized(name)}) ::continue:: end - dialogs.showListPrompt('party', "Select someone to add to your \"Core Party\" (able to assume control, able to unretire):", COLOR_WHITE, + dialogs.showListPrompt('party', 'Select someone to add to your "Core Party" (able to assume control, able to unretire):', COLOR_WHITE, choices, function(id, choice) addToCoreParty(choice.nemesis) end, nil, nil, true) diff --git a/internal/control-panel/registry.lua b/internal/control-panel/registry.lua index 83c4ac8b80..5f20954cc2 100644 --- a/internal/control-panel/registry.lua +++ b/internal/control-panel/registry.lua @@ -42,7 +42,7 @@ COMMANDS_BY_IDX = { conflicts={'cleanowned-nodump'}, params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', ']'}}, {command='cleanowned-nodump', group='automation', mode='repeat', - desc='Encourage dwarves to drop tattered clothing on the floor when there is new available clothing.', + desc='Drop tattered clothing, but don\'t mark it for dumping. Pairs well with tailor and tailor confiscate false.', conflicts={'cleanowned'}, params={'--time', '1', '--timeUnits', 'months', '--command', '[', 'cleanowned', 'X', 'nodump', ']'}}, {command='gui/settings-manager load-standing-orders', group='automation', mode='run', @@ -59,6 +59,8 @@ COMMANDS_BY_IDX = { {command='seedwatch', group='automation', mode='enable'}, {command='suspendmanager', group='automation', mode='enable'}, {command='tailor', group='automation', mode='enable'}, + {command='tailor confiscate false', group='automation', mode='run', + desc='Enable if you don\'t want old clothes to be dumped. Pairs well with cleanowned-nodump.'}, -- bugfix tools {command='adamantine-cloth-wear', help_command='tweak', group='bugfix', mode='tweak', default=true, @@ -69,6 +71,9 @@ COMMANDS_BY_IDX = { {command='fix/dead-units', group='bugfix', mode='repeat', default=true, desc='Fix units still being assigned to burrows after death.', params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dead-units', '--burrow', '-q', ']'}}, + {command='fix/dry-buckets', group='bugfix', mode='repeat', default=true, + desc='Allow discarded water buckets and clogged wells to be used again.', + params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'fix/dry-buckets', '-q', ']'}}, {command='fix/empty-wheelbarrows', group='bugfix', mode='repeat', default=true, desc='Make abandoned full wheelbarrows usable again.', params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/empty-wheelbarrows', '-q', ']'}}, diff --git a/internal/quickfort/zone.lua b/internal/quickfort/zone.lua index 0c46eb5ca0..dbb2522cf8 100644 --- a/internal/quickfort/zone.lua +++ b/internal/quickfort/zone.lua @@ -6,10 +6,11 @@ if not dfhack_flags.module then end require('dfhack.buildings') -- loads additional functions into dfhack.buildings -local utils = require('utils') +local preserve_rooms = require('plugins.preserve-rooms') local quickfort_common = reqscript('internal/quickfort/common') local quickfort_building = reqscript('internal/quickfort/building') local quickfort_parse = reqscript('internal/quickfort/parse') +local utils = require('utils') local log = quickfort_common.log local logfn = quickfort_common.logfn @@ -227,12 +228,6 @@ local function parse_location_props(props) return location_data end -local function get_noble_unit(noble) - local unit = dfhack.units.getUnitByNobleRole(noble) - if not unit then log('could not find a noble position for: "%s"', noble) end - return unit -end - local function parse_zone_config(c, props) if not rawget(zone_db_raw, c) then return 'Invalid', nil @@ -250,13 +245,7 @@ local function parse_zone_config(c, props) props.name = nil end if props.assigned_unit then - zone_data.assigned_unit = get_noble_unit(props.assigned_unit) - if not zone_data.assigned_unit and props.assigned_unit:lower() == 'sheriff' then - zone_data.assigned_unit = get_noble_unit('captain_of_the_guard') - end - if not zone_data.assigned_unit then - log('could not find a unit assigned to noble position: "%s"', props.assigned_unit) - end + zone_data.assigned_unit = props.assigned_unit props.assigned_unit = nil end if db_entry.props_fn then db_entry.props_fn(zone_data, props) end @@ -387,11 +376,12 @@ local function create_zone(zone, data, ctx) set_location(bld, data.location, ctx) data.location = nil end - if data.assigned_unit then - dfhack.buildings.setOwner(bld, data.assigned_unit) - data.assigned_unit = nil - end + local assigned_unit = data.assigned_unit + data.assigned_unit = nil utils.assign(bld, data) + if assigned_unit then + preserve_rooms.assignToRole(assigned_unit, bld) + end return ntiles end diff --git a/position.lua b/position.lua index 42c90136f3..3ca3bdd543 100644 --- a/position.lua +++ b/position.lua @@ -1,3 +1,19 @@ + +local cursor = df.global.cursor +local args = {...} +if #args > 0 then --Copy keyboard cursor to clipboard + if #args > 1 then + qerror('Too many arguments!') + elseif args[1] ~= '-c' and args[1] ~= '--copy' then + qerror('Invalid argument "'..args[1]..'"!') + elseif cursor.x < 0 then + qerror('No keyboard cursor!') + end + + dfhack.internal.setClipboardTextCp437(('%d,%d,%d'):format(cursor.x, cursor.y, cursor.z)) + return +end + local months = { 'Granite, in early Spring.', 'Slate, in mid Spring.', @@ -30,11 +46,15 @@ print('Time:') print(' The time is '..string.format('%02d:%02d:%02d', hour, minute, second)) print(' The date is '..string.format('%05d-%02d-%02d', df.global.cur_year, month, day)) print(' It is the month of '..months[month]) ---TODO: print(' It is the Age of '..age_name) + +local eras = df.global.world.history.eras +if #eras > 0 then + print(' It is the '..eras[#eras-1].title.name..'.') +end print('Place:') print(' The z-level is z='..df.global.window_z) -print(' The cursor is at x='..df.global.cursor.x..', y='..df.global.cursor.y) +print(' The cursor is at x='..cursor.x..', y='..cursor.y) print(' The window is '..df.global.gps.dimx..' tiles wide and '..df.global.gps.dimy..' tiles high') if df.global.gps.mouse_x == -1 then print(' The mouse is not in the DF window') else print(' The mouse is at x='..df.global.gps.mouse_x..', y='..df.global.gps.mouse_y..' within the window') end diff --git a/timestream.lua b/timestream.lua index bf69f2a476..542ed93ff9 100644 --- a/timestream.lua +++ b/timestream.lua @@ -319,7 +319,7 @@ local function adjust_activities(timeskip) elseif df.activity_event_writest:is_instance(ev) then decrement_counter(ev, 'timer', timeskip) elseif df.activity_event_copy_written_contentst:is_instance(ev) then - decrement_counter(ev, 'time_left', timeskip) + decrement_counter(ev, 'timer', timeskip) elseif df.activity_event_make_believest:is_instance(ev) then decrement_counter(ev, 'time_left', timeskip) elseif df.activity_event_play_with_toyst:is_instance(ev) then