Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: DFHack/scripts
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: 6c110aad9ea0899e1a94ac615a058061ad2d04d8
Choose a base ref
...
head repository: DFHack/scripts
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: 1880f4a9d528f8ae3a39b619bca89520185061ef
Choose a head ref
  • 11 commits
  • 7 files changed
  • 2 contributors

Commits on Dec 30, 2023

  1. create registry of commands

    myk002 committed Dec 30, 2023

    Verified

    This commit was signed with the committer’s verified signature.
    Dream-Master Martin Robertz
    Copy the full SHA
    b27ad1b View commit details
  2. alphabetize

    myk002 committed Dec 30, 2023
    Copy the full SHA
    08fb6dc View commit details
  3. implement control panel cli

    myk002 committed Dec 30, 2023
    Copy the full SHA
    13ed3b1 View commit details
  4. get Preferences tab working

    myk002 committed Dec 30, 2023
    Copy the full SHA
    cd64f6e View commit details
  5. get overlay tab working

    myk002 committed Dec 30, 2023
    Copy the full SHA
    83d6955 View commit details
  6. Copy the full SHA
    8057eed View commit details
  7. everything mostly works now

    myk002 committed Dec 30, 2023
    Copy the full SHA
    38d1a18 View commit details
  8. implement migration

    myk002 committed Dec 30, 2023
    Copy the full SHA
    601da49 View commit details
  9. finish docs

    myk002 committed Dec 30, 2023
    Copy the full SHA
    e07779f View commit details
  10. fix doc typo

    myk002 committed Dec 30, 2023
    Copy the full SHA
    31683dd View commit details

Commits on Dec 31, 2023

  1. Merge pull request #908 from myk002/myk_control_panel

    Control panel v2
    myk002 authored Dec 31, 2023
    Copy the full SHA
    1880f4a View commit details
Showing with 1,413 additions and 677 deletions.
  1. +231 −0 control-panel.lua
  2. +69 −0 docs/control-panel.rst
  3. +64 −65 docs/gui/control-panel.rst
  4. +554 −612 gui/control-panel.lua
  5. +192 −0 internal/control-panel/common.lua
  6. +131 −0 internal/control-panel/migration.lua
  7. +172 −0 internal/control-panel/registry.lua
231 changes: 231 additions & 0 deletions control-panel.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,231 @@
--@module = true

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')

local GLOBAL_KEY = 'control-panel'

-- state change hooks

local function apply_system_config()
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)
end
end
for _, data in ipairs(registry.PREFERENCES_BY_IDX) do
local value = safe_index(common.config.data.preferences, data.name, 'val')
if value ~= nil then
data.set_fn(value)
end
end
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
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
apply_autostart_config()
persist.GlobalTable[GLOBAL_KEY] = json.encode({autostart_done=true})
end
local enabled_repeats = json.decode(persist.GlobalTable[common.REPEATS_GLOBAL_KEY] or '')
for _, data in ipairs(registry.COMMANDS_BY_IDX) do
if data.mode == 'repeat' and enabled_repeats[data.command] then
common.apply_command(data)
end
end
end

dfhack.onStateChange[GLOBAL_KEY] = function(sc)
if sc == SC_CORE_INITIALIZED then
apply_system_config()
elseif sc == SC_MAP_LOADED and dfhack.world.isFortressMode() then
apply_fort_loaded_config()
end
end


-- CLI

local function print_header(header)
print()
print(header)
print(('-'):rep(#header))
end

local function list_command_group(group, filter_strs, enabled_map)
local header = ('Group: %s'):format(group)
for idx, data in ipairs(registry.COMMANDS_BY_IDX) do
local first_word = common.get_first_word(data.command)
if not common.command_passes_filters(data, group, first_word, filter_strs) then
goto continue
end
if header then
print_header(header)
---@diagnostic disable-next-line: cast-local-type
header = nil
end
local extra = ''
if data.mode == 'system_enable' then
extra = ' (global)'
end
print(('%d) %s%s'):format(idx, data.command, extra))
local desc = common.get_description(data)
if #desc > 0 then
print((' %s'):format(desc))
end
local default_value = not not data.default
local current_value = safe_index(common.config.data.commands, data.command, 'autostart')
if current_value == nil then
current_value = default_value
end
print((' autostart enabled: %s (default: %s)'):format(current_value, default_value))
if enabled_map[data.command] ~= nil then
print((' currently enabled: %s'):format(enabled_map[data.command]))
end
print()
::continue::
end
if not header then
end
end

local function list_preferences(filter_strs)
local header = 'Preferences'
for _, data in ipairs(registry.PREFERENCES_BY_IDX) do
local search_key = ('%s %s %s'):format(data.name, data.label, data.desc)
if not utils.search_text(search_key, filter_strs) then goto continue end
if header then
print_header(header)
---@diagnostic disable-next-line: cast-local-type
header = nil
end
print(('%s) %s'):format(data.name, data.label))
print((' %s'):format(data.desc))
print((' current: %s (default: %s)'):format(data.get_fn(), data.default))
if data.min then
print((' minimum: %s'):format(data.min))
end
print()
::continue::
end
end

local function do_list(filter_strs)
local enabled_map =common.get_enabled_map()
list_command_group('automation', filter_strs, enabled_map)
list_command_group('bugfix', filter_strs, enabled_map)
list_command_group('gameplay', filter_strs, enabled_map)
list_preferences(filter_strs)
end

local function get_command_data(name_or_idx)
if type(name_or_idx) == 'number' then
return registry.COMMANDS_BY_IDX[name_or_idx]
end
return registry.COMMANDS_BY_NAME[name_or_idx]
end

local function do_enable_disable(which, entries)
local enabled_map =common.get_enabled_map()
for _, entry in ipairs(entries) do
local data = get_command_data(entry)
if data.mode ~= 'system_enable' and not dfhack.world.isFortressMode() then
qerror('must have a loaded fortress to enable '..data.name)
end
if common.apply_command(data, enabled_map, which == 'en') then
print(('%sabled %s'):format(which, entry))
end
end
end

local function do_enable(entries)
do_enable_disable('en', entries)
end

local function do_disable(entries)
do_enable_disable('dis', entries)
end

local function do_autostart_noautostart(which, entries)
for _, entry in ipairs(entries) do
local data = get_command_data(entry)
if not data then
qerror(('autostart command or index not found: "%s"'):format(entry))
else
common.set_autostart(data, which == 'en')
print(('%sabled autostart for: %s'):format(which, entry))
end
end
common.config:write()
end

local function do_autostart(entries)
do_autostart_noautostart('en', entries)
end

local function do_noautostart(entries)
do_autostart_noautostart('dis', entries)
end

local function do_set(params)
local name, value = params[1], params[2]
local data = registry.PREFERENCES_BY_NAME[name]
if not data then
qerror(('preference name not found: "%s"'):format(name))
end
common.set_preference(data, value)
common.config:write()
end

local function do_reset(params)
local name = params[1]
local data = registry.PREFERENCES_BY_NAME[name]
if not data then
qerror(('preference name not found: "%s"'):format(name))
end
common.set_preference(data, data.default)
common.config:write()
end

local command_switch = {
list=do_list,
enable=do_enable,
disable=do_disable,
autostart=do_autostart,
noautostart=do_noautostart,
set=do_set,
reset=do_reset,
}

local function main(args)
local help = false

local positionals = argparse.processArgsGetopt(args, {
{'h', 'help', handler=function() help = true end},
})

local command = table.remove(positionals, 1)
if help or not command or not command_switch[command] then
print(dfhack.script_help())
return
end

command_switch[command](positionals)
end

if not dfhack_flags.module then
main{...}
end
69 changes: 69 additions & 0 deletions docs/control-panel.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
control-panel
=============

.. dfhack-tool::
:summary: Configure DFHack and manage active DFHack tools.
:tags: dfhack

This is the commandline interface for configuring DFHack behavior, toggling
which functionality is enabled right now, and setting up which tools are
enabled/run when starting new fortress games. For an in-game
graphical interface, please use `gui/control-panel`. For a commandline
interface for configuring which overlays are enabled, please use `overlay`.

This interface controls three kinds of configuration:

1. Tools that are enabled right now. These are DFHack tools that run in the
background, like `autofarm`, or tools that DFHack can run on a repeating
schedule, like the "autoMilk" functionality of `workorder`. Most tools that can
be enabled are saved with your fort, so you can have different tools enabled
for different forts. If a tool is marked "global", however, like
`hide-tutorials`, then enabling it will make it take effect for all games.

2. Tools or commands that should be auto-enabled or auto-run when you start a
new fortress. In addition to tools that can be "enabled", this includes
commands that you might want to run once just after you embark, such as
commands to configure `autobutcher` or to drain portions of excessively deep
aquifers.

3. DFHack system preferences, such as whether "Armok" (god-mode) tools are
shown in DFHack lists (including the lists of commands shown by the control
panel) or mouse configuration like how fast you have to click for it to count
as a double click (for example, when maximizing DFHack tool windows).
Preferences are "global" in that they apply to all games.

Run ``control-panel list`` to see the current settings and what tools and
preferences are available for configuration.

Usage
-----

::

control-panel list <search string>
control-panel enable|disable <command or number from list>
control-panel autostart|noautostart <command or number from list>
control-panel set <preference> <value>
control-panel reset <preference>

Examples
--------
``control-panel list butcher``
Shows the current configuration of all commands related to `autobutcher`
(and anything else that includes the text "butcher" in it).
``control-panel enable fix/empty-wheelbarrows`` or ``control-panel enable 25``
Starts to run `fix/empty-wheelbarrows` periodically to maintain the
usability of your wheelbarrows. In the second version of this command, the
number "25" is used as an example. You'll have to run
``control-panel list`` to see what number this command is actually listed
as.
``control-panel autostart autofarm``
Configures `autofarm` to become automatically enabled when you start a new
fort.
``control-panel autostart fix/blood-del``
Configures `fix/blood-del` to run once when you start a new fort.
``control-panel set HIDE_ARMOK_TOOLS true``
Enable "mortal mode" and hide "armok" tools in the DFHack UIs. Note that
this will also remove some entries from the ``control-panel list`` output.
Run ``control-panel list`` to see all preference options and their
descriptions.
129 changes: 64 additions & 65 deletions docs/gui/control-panel.rst
Original file line number Diff line number Diff line change
@@ -2,83 +2,82 @@ gui/control-panel
=================

.. dfhack-tool::
:summary: Configure DFHack.
:summary: Configure DFHack and manage active DFHack tools.
:tags: dfhack

The DFHack control panel allows you to quickly see and change what DFHack tools
are enabled now, which tools will run when you start a new fort, and how global
DFHack configuration options are set. It also provides convenient links to
relevant help pages and GUI configuration frontends. The control panel has
several pages that you can switch among by clicking on the tabs at the top of
the window. Each page has a search filter so you can quickly find the tools and
options that you're looking for.

Fort Services
-------------

The fort services page shows tools that you can enable in fort mode. You can
select the tool name to see a short description at the bottom of the list. Hit
are enabled, which tools will run when you start a new fort, which UI overlays
are enabled, and how global DFHack configuration options are set. It also
provides convenient links to relevant help pages and GUI configuration
frontends (where available). The control panel has several sections that you
can access by clicking on the tabs at the top of the window. Each tab has a
search filter so you can quickly find the tools and options that you're looking
for.

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 "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
: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 the fort services displayed on this page can only be enabled when a
fort is loaded. They will be disabled in the list and cannot be enabled or have
their GUI config screens shown until you have loaded a fortress. Once you do
enable them (after you've loaded a fort), they will save their state with your
fort and automatically re-enable themselves when you load your fort again.
Note that before a fort is loaded, there will be very few tools listed here.

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
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 :).

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
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` or click on that icon 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:

New Fort Autostart Commands
---------------------------

This page shows the tools that you can configure DFHack to auto-enable or
auto-run when you start a new fort. You'll recognize many tools from the
previous page 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 <ban-cooking>`.

Periodic Maintenance Operations
-------------------------------

This page shows commands that DFHack can regularly run for you in order to keep
your fort (and the game) running smoothly. For example, there are commands to
periodically enqueue orders for shearing animals that are ready to be shorn or
sort your manager orders so slow-moving daily orders won't prevent your
high-volume one-time orders from ever being completed.

System Services
---------------

The system services page shows "core" DFHack tools that provide background
services to other tools. It is generally not advisable to turn these tools
off. If you do toggle them off in the control panel, they will be re-enabled
when you restart the game. If you really need to turn these tools off
permanently, add a line like ``disable toolname`` to your
``dfhack-config/init/dfhack.init`` file.

Overlays
--------

The overlays page allows you to easily see which overlays are enabled and lets
you toggle them on and off and see the help for the owning tools. 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`.

Preferences
-----------

The preferences page allows you to change DFHack's internal settings and
defaults, like whether DFHack tools pause the game when they come up, or how
long you can wait between clicks and still have it count as a double-click. Hit
:kbd:`Ctrl`:kbd:`G` or click on the hotkey hint at the bottom of the page to
restore all preferences to defaults.
The "Autostart" tab
-------------------

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 <ban-cooking>`.

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`.

The "Preferences" tab
---------------------

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.
Click on the gear icon or hit :kbd:`Enter` to toggle or edit the selected
preference.

Usage
-----
1,166 changes: 554 additions & 612 deletions gui/control-panel.lua

Large diffs are not rendered by default.

192 changes: 192 additions & 0 deletions internal/control-panel/common.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
--@module = true

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')

local CONFIG_FILE = 'dfhack-config/control-panel.json'

REPEATS_GLOBAL_KEY = 'control-panel-repeats'

local function get_config()
local f = json.open(CONFIG_FILE)
local updated = false
-- ensure proper structure
ensure_key(f.data, 'commands')
ensure_key(f.data, 'preferences')
if f.exists then
-- remove unknown or out of date entries from the loaded config
for k in pairs(f.data) do
if k ~= 'commands' and k ~= 'preferences' then
updated = true
f.data[k] = nil
end
end
for name, config_command_data in pairs(f.data.commands) do
local data = registry.COMMANDS_BY_NAME[name]
if not data or config_command_data.version ~= data.version then
updated = true
f.data.commands[name] = nil
end
end
for name, config_pref_data in pairs(f.data.preferences) do
local data = registry.PREFERENCES_BY_NAME[name]
if not data or config_pref_data.version ~= data.version then
updated = true
f.data.preferences[name] = nil
end
end
else
-- migrate any data from old configs
migration.migrate(f.data)
updated = next(f.data.commands) or next(f.data.preferences)
end
if updated then
f:write()
end
return f
end

config = config or get_config()

local function unmunge_repeat_name(munged_name)
if munged_name:startswith('control-panel/') then
return munged_name:sub(15)
end
end

function get_enabled_map()
local enabled_map = {}
local output = dfhack.run_command_silent('enable'):split('\n+')
for _,line in ipairs(output) do
local _,_,command,enabled_str = line:find('%s*(%S+):%s+(%S+)')
if enabled_str then
enabled_map[command] = enabled_str == 'on'
end
end
-- repeat entries override tool names for control-panel
for munged_name in pairs(repeatUtil.repeating) do
local name = unmunge_repeat_name(munged_name)
if name then
enabled_map[name] = true
end
end
return enabled_map
end

function get_first_word(str)
local word = str:trim():split(' +')[1]
if word:startswith(':') then word = word:sub(2) end
return word
end

function command_passes_filters(data, target_group, filter_strs)
if data.group ~= target_group then
return false
end
filter_strs = filter_strs or {}
local first_word = get_first_word(data.command)
if dfhack.getHideArmokTools() and helpdb.is_entry(first_word)
and helpdb.get_entry_tags(first_word).armok
then
return false
end
if not utils.search_text(data.command, filter_strs) then
return false
end
return true
end

function get_description(data)
if data.desc then
return data.desc
end
local first_word = get_first_word(data.command)
return helpdb.is_entry(first_word) and helpdb.get_entry_short_help(first_word) or ''
end

local function persist_enabled_repeats()
local cp_repeats = {}
for munged_name in pairs(repeatUtil.repeating) do
local name = unmunge_repeat_name(munged_name)
if name then
cp_repeats[name] = true
end
end
persist.GlobalTable[REPEATS_GLOBAL_KEY] = json.encode(cp_repeats)
end

function apply_command(data, enabled_map, enabled)
enabled_map = enabled_map or {}
if enabled == nil then
enabled = safe_index(config.data.commands, data.command, 'autostart')
enabled = enabled or (enabled == nil and data.default)
if not enabled then return end
end
if data.mode == 'enable' or data.mode == 'system_enable' then
if enabled_map[data.command] == nil then
dfhack.printerr(('tool not enableable: "%s"'):format(data.command))
return false
else
dfhack.run_command({enabled and 'enable' or 'disable', data.command})
end
elseif data.mode == 'repeat' then
local munged_name = 'control-panel/' .. data.command
if enabled then
local command_str = ('repeat --name %s %s\n'):
format(munged_name, table.concat(data.params, ' '))
dfhack.run_command(command_str)
else
repeatUtil.cancel(munged_name)
end
persist_enabled_repeats()
elseif data.mode == 'run' then
if enabled then
dfhack.run_command(data.command)
end
else
dfhack.printerr(('unhandled command: "%s"'):format(data.command))
return false
end
return true
end

function set_preference(data, in_value)
local expected_type = type(data.default)
local value = in_value
if expected_type == 'boolean' and type(value) ~= 'boolean' then
value = argparse.boolean(value)
end
local actual_type = type(value)
if actual_type ~= expected_type then
qerror(('"%s" has an unexpected value type: got: %s; expected: %s'):format(
in_value, actual_type, expected_type))
end
if data.min and data.min > value then
qerror(('value too small: got: %s; minimum: %s'):format(value, data.min))
end
data.set_fn(value)
if data.default ~= value then
config.data.preferences[data.name] = {
val=value,
version=data.version,
}
else
config.data.preferences[data.name] = nil
end
end

function set_autostart(data, enabled)
if enabled ~= not not data.default then
config.data.commands[data.command] = {
autostart=enabled,
version=data.version,
}
else
config.data.commands[data.command] = nil
end
end
131 changes: 131 additions & 0 deletions internal/control-panel/migration.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
-- migrate configuration from 50.11-r4 and prior to new format
--@module = true

-- read old files, add converted data to config_data, overwrite old files with
-- a message that says they are deprecated and can be deleted with the proper
-- procedure. we can't delete them outright since steam may just restore them due to
-- Steam Cloud. We *could* delete them, though, if we know that we've been started
-- from Steam as DFHack and not as DF

local argparse = require('argparse')
local registry = reqscript('internal/control-panel/registry')

-- init files
local SYSTEM_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-system.init'
local AUTOSTART_FILE = 'dfhack-config/init/onMapLoad.control-panel-new-fort.init'
local REPEATS_FILE = 'dfhack-config/init/onMapLoad.control-panel-repeats.init'
local PREFERENCES_INIT_FILE = 'dfhack-config/init/dfhack.control-panel-preferences.init'

local function save_tombstone_file(path)
local ok, f = pcall(io.open, path, 'w')
if not ok or not f then
dialogs.showMessage('Error',
('Cannot open file for writing: "%s"'):format(path))
return
end
f:write('# This file was once used by gui/control-panel\n')
f:write('# If you are on Steam, you can delete this file manually\n')
f:write('# by starting DFHack in the Steam client, then deleting\n')
f:write('# this file while DF is running. Otherwise Steam Cloud will\n')
f:write('# restore the file when you next run DFHack.\n')
f:write('#\n')
f:write('# If you\'re not on Steam, you can delete this file at any time.\n')
f:close()
end

local function add_autostart(config_data, name)
if not registry.COMMANDS_BY_NAME[name].default then
config_data.commands[name] = {autostart=true}
end
end

local function add_preference(config_data, name, val)
local data = registry.PREFERENCES_BY_NAME[name]
if type(data.default) == 'boolean' then
ok, val = pcall(argparse.boolean, val)
if not ok then return end
elseif type(data.default) == 'number' then
val = tonumber(val)
if not val then return end
end
if data.default ~= val then
config_data.preferences[name] = {val=val}
end
end

local function parse_lines(fname, line_fn)
local ok, f = pcall(io.open, fname)
if not ok or not f then return end
for line in f:lines() do
line = line:trim()
if #line > 0 and not line:startswith('#') then
line_fn(line)
end
end
end

local function migrate_system(config_data)
parse_lines(SYSTEM_INIT_FILE, function(line)
local service = line:match('^enable ([%S]+)$')
if not service then return end
local data = registry.COMMANDS_BY_NAME[service]
if data and (data.mode == 'system_enable' or data.command == 'work-now') then
add_autostart(config_data, service)
end
end)
save_tombstone_file(SYSTEM_INIT_FILE)
end

local function migrate_autostart(config_data)
parse_lines(AUTOSTART_FILE, function(line)
local service = line:match('^on%-new%-fortress enable ([%S]+)$')
or line:match('^on%-new%-fortress (.+)')
if not service then return end
local data = registry.COMMANDS_BY_NAME[service]
if data and (data.mode == 'enable' or data.mode == 'run') then
add_autostart(config_data, service)
end
end)
save_tombstone_file(AUTOSTART_FILE)
end

local REPEAT_MAP = {
autoMilkCreature='automilk',
autoShearCreature='autoshear',
['dead-units-burrow']='fix/dead-units',
['empty-wheelbarrows']='fix/empty-wheelbarrows',
['general-strike']='fix/general-strike',
['stuck-instruments']='fix/stuck-instruments',
}

local function migrate_repeats(config_data)
parse_lines(REPEATS_FILE, function(line)
local service = line:match('^repeat %-%-name ([%S]+)')
if not service then return end
service = REPEAT_MAP[service] or service
local data = registry.COMMANDS_BY_NAME[service]
if data and data.mode == 'repeat' then
add_autostart(config_data, service)
end
end)
save_tombstone_file(REPEATS_FILE)
end

local function migrate_preferences(config_data)
parse_lines(PREFERENCES_INIT_FILE, function(line)
local name, val = line:match('^:lua .+%.([^=]+)=(.+)')
if not name or not val then return end
local data = registry.PREFERENCES_BY_NAME[name]
if data then
add_preference(config_data, name, val)
end
end)
save_tombstone_file(PREFERENCES_INIT_FILE)
end

function migrate(config_data)
migrate_system(config_data)
migrate_autostart(config_data)
migrate_repeats(config_data)
migrate_preferences(config_data)
end
172 changes: 172 additions & 0 deletions internal/control-panel/registry.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
--@module = true

local gui = require('gui')
local widgets = require('gui.widgets')
local utils = require('utils')

-- please keep in alphabetical order per group
-- add a 'version' attribute if we want to reset existing configs for a command to the default
COMMANDS_BY_IDX = {
-- automation tools
{command='autobutcher', group='automation', mode='enable'},
{command='autobutcher target 10 10 14 2 BIRD_GOOSE', group='automation', mode='run',
desc='Enable if you usually want to raise geese.'},
{command='autobutcher target 10 10 14 2 BIRD_TURKEY', group='automation', mode='run',
desc='Enable if you usually want to raise turkeys.'},
{command='autobutcher target 10 10 14 2 BIRD_CHICKEN', group='automation', mode='run',
desc='Enable if you usually want to raise chickens.'},
{command='autochop', group='automation', mode='enable'},
{command='autoclothing', group='automation', mode='enable'},
{command='autofarm', group='automation', mode='enable'},
{command='autofarm threshold 150 grass_tail_pig', group='automation', mode='run',
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',
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',
desc='Automatically shear creatures that are ready for shearing.',
params={'--time', '14', '--timeUnits', 'days', '--command', '[', 'workorder', 'ShearCreature', ']'}},
{command='autoslab', group='automation', mode='enable'},
{command='ban-cooking all', group='automation', mode='run'},
{command='buildingplan set boulders false', group='automation', mode='run',
desc='Enable if you usually don\'t want to use boulders for construction.'},
{command='buildingplan set logs false', group='automation', mode='run',
desc='Enable if you usually don\'t want to use logs for construction.'},
{command='cleanowned', group='automation', mode='repeat',
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',
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},
{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/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', ']'}},
{command='fix/general-strike', group='bugfix', mode='repeat', default=true,
desc='Prevent dwarves from getting stuck and refusing to work.',
params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/general-strike', '-q', ']'}},
{command='fix/protect-nicks', group='bugfix', mode='enable', default=true},
{command='fix/stuck-instruments', group='bugfix', mode='repeat', default=true,
desc='Fix activity references on stuck instruments to make them usable again.',
params={'--time', '1', '--timeUnits', 'days', '--command', '[', 'fix/stuck-instruments', ']'}},
{command='preserve-tombs', group='bugfix', mode='enable', default=true},

-- gameplay tools
{command='combine', group='gameplay', mode='repeat',
desc='Combine partial stacks in stockpiles into full stacks.',
params={'--time', '7', '--timeUnits', 'days', '--command', '[', 'combine', 'all', '-q', ']'}},
{command='drain-aquifer --top 2', group='gameplay', mode='run',
desc='Ensure that your maps have no more than 2 layers of aquifer.'},
{command='dwarfvet', group='gameplay', mode='enable'},
{command='emigration', group='gameplay', mode='enable'},
{command='fastdwarf', group='gameplay', mode='enable'},
{command='hermit', group='gameplay', mode='enable'},
{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',
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'},
{command='warn-starving', group='gameplay', mode='repeat',
desc='Show a warning dialog when units are starving or dehydrated.',
params={'--time', '10', '--timeUnits', 'days', '--command', '[', 'warn-starving', ']'}},
{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', ']'}},
}

COMMANDS_BY_NAME = {}
for _,data in ipairs(COMMANDS_BY_IDX) do
COMMANDS_BY_NAME[data.command] = data
end

-- keep in desired display order
PREFERENCES_BY_IDX = {
{
name='HIDE_ARMOK_TOOLS',
label='Mortal mode: hide "armok" tools',
desc='Don\'t show tools that give you god-like powers wherever DFHack tools are listed.',
default=false,
get_fn=function() return dfhack.HIDE_ARMOK_TOOLS end,
set_fn=function(val) dfhack.HIDE_ARMOK_TOOLS = val end,
},
{
name='FILTER_FULL_TEXT',
label='DFHack searches full text',
desc='When searching, whether to match anywhere in the text (true) or just at the start of words (false).',
default=false,
get_fn=function() return utils.FILTER_FULL_TEXT end,
set_fn=function(val) utils.FILTER_FULL_TEXT = val end,
},
{
name='HIDE_CONSOLE_ON_STARTUP',
label='Hide console on startup (MS Windows only)',
desc='Hide the external DFHack terminal window on startup. Use the "show" command to unhide it.',
default=true,
get_fn=function() return dfhack.HIDE_CONSOLE_ON_STARTUP end,
set_fn=function(val) dfhack.HIDE_CONSOLE_ON_STARTUP = val end,
},
{
name='DEFAULT_INITIAL_PAUSE',
label='DFHack tools autopause game',
desc='Always pause the game when a DFHack tool window is shown (you can still unpause afterwards).',
default=true,
get_fn=function() return gui.DEFAULT_INITIAL_PAUSE end,
set_fn=function(val) gui.DEFAULT_INITIAL_PAUSE = val end,
},
{
name='INTERCEPT_HANDLED_HOTKEYS',
label='Intercept handled hotkeys',
desc='Prevent key events handled by DFHack windows from also affecting the vanilla widgets.',
default=true,
get_fn=dfhack.internal.getSuppressDuplicateKeyboardEvents,
set_fn=dfhack.internal.setSuppressDuplicateKeyboardEvents,
},
{
name='DOUBLE_CLICK_MS',
label='Mouse double click speed (ms)',
desc='How long to wait for the second click of a double click, in ms.',
default=500,
min=50,
get_fn=function() return widgets.DOUBLE_CLICK_MS end,
set_fn=function(val) widgets.DOUBLE_CLICK_MS = val end,
},
{
name='SCROLL_DELAY_MS',
label='Mouse scroll repeat delay (ms)',
desc='The delay between events when holding the mouse button down on a scrollbar, in ms.',
default=20,
min=5,
get_fn=function() return widgets.SCROLL_DELAY_MS end,
set_fn=function(val) widgets.SCROLL_DELAY_MS = val end,
},
{
name='SCROLL_INITIAL_DELAY_MS',
label='Mouse initial scroll repeat delay (ms)',
desc='The delay before scrolling quickly when holding the mouse button down on a scrollbar, in ms.',
default=300,
min=5,
get_fn=function() return widgets.SCROLL_INITIAL_DELAY_MS end,
set_fn=function(val) widgets.SCROLL_INITIAL_DELAY_MS = val end,
},
}

PREFERENCES_BY_NAME = {}
for _,data in ipairs(PREFERENCES_BY_IDX) do
PREFERENCES_BY_NAME[data.name] = data
end