Merge branch 'develop' into docs

develop
myk002 2022-08-15 17:09:36 -07:00
commit df9c37a8b7
No known key found for this signature in database
GPG Key ID: 8A39CA0FA0C16E78
22 changed files with 1428 additions and 360 deletions

@ -86,6 +86,7 @@ jobs:
-DBUILD_TESTS:BOOL=ON \
-DBUILD_DEV_PLUGINS:BOOL=${{ matrix.plugins == 'all' }} \
-DBUILD_SIZECHECK:BOOL=${{ matrix.plugins == 'all' }} \
-DBUILD_SKELETON:BOOL=${{ matrix.plugins == 'all' }} \
-DBUILD_STONESENSE:BOOL=${{ matrix.plugins == 'all' }} \
-DBUILD_SUPPORTED:BOOL=1 \
-DCMAKE_C_COMPILER_LAUNCHER=ccache \

@ -22,7 +22,7 @@ Plugins
DFHack plugins are written in C++ and located in the ``plugins`` folder.
Currently, documentation on how to write plugins is somewhat sparse. There are
templates that you can use to get started in the ``plugins/skeleton``
templates that you can use to get started in the ``plugins/examples``
folder, and the source code of existing plugins can also be helpful.
If you want to compile a plugin that you have just added, you will need to add a
@ -35,7 +35,7 @@ other commands).
Plugins can also register handlers to run on every tick, and can interface with
the built-in `enable` and `disable` commands. For the full plugin API, see the
skeleton plugins or ``PluginManager.cpp``.
example ``skeleton`` plugin or ``PluginManager.cpp``.
Installed plugins live in the ``hack/plugins`` folder of a DFHack installation,
and the `load` family of commands can be used to load a recompiled plugin

@ -3043,6 +3043,18 @@ parameters.
function also verifies that the coordinates are valid for the current map and
throws if they are not (unless ``skip_validation`` is set to true).
* ``argparse.positiveInt(arg, arg_name)``
Throws if ``tonumber(arg)`` is not a positive integer; otherwise returns
``tonumber(arg)``. If ``arg_name`` is specified, it is used to make error
messages more useful.
* ``argparse.nonnegativeInt(arg, arg_name)``
Throws if ``tonumber(arg)`` is not a non-negative integer; otherwise returns
``tonumber(arg)``. If ``arg_name`` is specified, it is used to make error
messages more useful.
dumper
======
@ -3056,6 +3068,87 @@ function:
argument specifies the indentation step size in spaces. For
the other arguments see the original documentation link above.
helpdb
======
Unified interface for DFHack tool help text. Help text is read from the rendered
text in ``hack/docs/docs/``. If no rendered text exists, help is read from the
script sources (for scripts) or the string passed to the ``PluginCommand``
initializer (for plugins). See `documentation` for details on how DFHack's help
system works.
The database is lazy-loaded when an API method is called. It rechecks its help
sources for updates if an API method has not been called in the last 60 seconds.
Each entry has several properties associated with it:
- The entry name, which is the name of a plugin, script, or command provided by
a plugin.
- The entry types, which can be ``builtin``, ``plugin``, and/or ``command``.
Entries for built-in commands (like ``ls`` or ``quicksave``) are both type
``builtin`` and ``command``. Entries named after plugins are type ``plugin``,
and if that plugin also provides a command with the same name as the plugin,
then the entry is also type ``command``. Entry types are returned as a map
of one or more of the type strings to ``true``.
- Short help, a the ~54 character description string.
- Long help, the entire contents of the associated help file.
- A list of tags that define the groups that the entry belongs to.
* ``helpdb.is_entry(str)``, ``helpdb.is_entry(list)``
Returns whether the given string (or list of strings) is an entry (are all
entries) in the db.
* ``helpdb.get_entry_types(entry)``
Returns the set (that is, a map of string to ``true``) of entry types for the
given entry.
* ``helpdb.get_entry_short_help(entry)``
Returns the short (~54 character) description for the given entry.
* ``helpdb.get_entry_long_help(entry)``
Returns the full help text for the given entry.
* ``helpdb.get_entry_tags(entry)``
Returns the set of tag names for the given entry.
* ``helpdb.is_tag(str)``, ``helpdb.is_tag(list)``
Returns whether the given string (or list of strings) is a (are all) valid tag
name(s).
* ``helpdb.get_tags()``
Returns the full alphabetized list of valid tag names.
* ``helpdb.get_tag_data(tag)``
Returns a list of entries that have the given tag. The returned table also
has a ``description`` key that contains the string description of the tag.
* ``helpdb.search_entries([include[, exclude]])``
Returns a list of names for entries that match the given filters. The list is
alphabetized by their last path component, with populated path components
coming before null path components (e.g. ``autobutcher`` will immediately
follow ``gui/autobutcher``).
The optional ``include`` and ``exclude`` filter params are maps with the
following elements:
:str: if a string, filters by the given substring. if a table of strings,
includes entry names that match any of the given substrings.
:tag: if a string, filters by the given tag name. if a table of strings,
includes entries that match any of the given tags.
:entry_type: if a string, matches entries of the given type. if a table of
strings, includes entries that match any of the given types.
If ``include`` is ``nil`` or empty, then all entries are included. If
``exclude`` is ``nil`` or empty, then no entries are filtered out.
profiler
========
@ -3904,9 +3997,9 @@ and then call the ``on_submit`` callback. Pressing the Escape key will also
release keyboard focus, but first it will restore the text that was displayed
before the ``EditField`` gained focus and then call the ``on_change`` callback.
The ``EditField`` cursor can be moved to where you want to insert/remove text
by clicking the mouse at that position. In addition, the following cursor
movement keys are recognized:
The ``EditField`` cursor can be moved to where you want to insert/remove text.
You can click where you want the cursor to move or you can use any of the
following keyboard hotkeys:
- Left/Right arrow: move the cursor one character to the left or right.
- Ctrl-Left/Right arrow: move the cursor one word to the left or right.
@ -4075,7 +4168,7 @@ HotkeyLabel class
-----------------
This Label subclass is a convenience class for formatting text that responds to
a hotkey.
a hotkey or mouse click.
It has the following attributes:
@ -4085,13 +4178,13 @@ It has the following attributes:
:label: The string (or a function that returns a string) to display after the
hotkey.
:on_activate: If specified, it is the callback that will be called whenever
the hotkey is pressed.
the hotkey is pressed or the label is clicked.
CycleHotkeyLabel class
----------------------
This Label subclass represents a group of related options that the user can
cycle through by pressing a specified hotkey.
cycle through by pressing a specified hotkey or clicking on the text.
It has the following attributes:
@ -4134,7 +4227,8 @@ This is a specialized subclass of CycleHotkeyLabel that has two options:
List class
----------
The List widget implements a simple list with paging.
The List widget implements a simple list with paging. You can click on a list
item to call the ``on_submit`` callback for that item.
It has the following attributes:
@ -4145,10 +4239,10 @@ It has the following attributes:
:on_select: Selection change callback; called as ``on_select(index,choice)``.
This is also called with *nil* arguments if ``setChoices`` is called
with an empty list.
:on_submit: Enter key or mouse click callback; if specified, the list calls it
as ``on_submit(index,choice)``.
:on_submit2: Shift-Enter key callback; if specified, the list calls it as
``on_submit2(index,choice)``.
:on_submit: Enter key or mouse click callback; if specified, the list reacts to the
key/click and calls the callback as ``on_submit(index,choice)``.
:on_submit2: Shift-Enter key callback; if specified, the list reacts to the key
and calls it as ``on_submit2(index,choice)``.
:row_height: Height of every row in text lines.
:icon_width: If not *nil*, the specified number of character columns
are reserved to the left of the list item for the icons.

@ -51,6 +51,9 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- ``materials.ItemTraitsDialog``: added a default ``on_select``-handler which toggles the traits.
- `orders`: added useful library of manager orders. see them with ``orders list`` and import them with, for example, ``orders import library/basic``
- `prospect`: add new ``--show`` option to give the player control over which report sections are shown. e.g. ``prospect all --show ores`` will just show information on ores.
- `seedwatch`: ``seedwatch all`` now adds all plants with seeds to the watchlist, not just the "basic" crops.
- UX: You can now move the cursor around in DFHack text fields in ``gui/`` scripts (e.g. `gui/blueprint`, `gui/quickfort`, or `gui/gm-editor`). You can move the cursor by clicking where you want it to go with the mouse or using the Left/Right arrow keys. Ctrl+Left/Right will move one word at a time, and Alt+Left/Right will move to the beginning/end of the text.
- UX: You can now click on the hotkey hint text in many ``gui/`` script windows to activate the hotkey, like a button. Not all scripts have been updated to use the clickable widget yet, but you can try it in `gui/blueprint` or `gui/quickfort`.
## Documentation
@ -63,7 +66,14 @@ changelog.txt uses a syntax similar to RST, with a few special sequences:
- ``Gui::getSelectedItem()``, ``Gui::getAnyItem()``: added support for the artifacts screen
## Lua
- History: added ``dfhack.getCommandHistory(history_id, history_filename)`` and ``dfhack.addCommandToHistory(history_id, history_filename, command)`` so gui scripts can access a commandline history without requiring a terminal.
- ``helpdb``: database and query interface for DFHack tool help text
- ``tile-material``: fix the order of declarations. The ``GetTileMat`` function now returns the material as intended (always returned nil before). Also changed the license info, with permission of the original author.
- ``widgets.EditField``: new ``onsubmit2`` callback attribute is called when the user hits Shift-Enter.
- ``widgets.EditField``: new function: ``setCursor(position)`` sets the input cursor.
- ``widgets.Label``: ``scroll`` function now interprets the keywords ``+page``, ``-page``, ``+halfpage``, and ``-halfpage`` in addition to simple positive and negative numbers.
- ``widgets.HotkeyLabel``: clicking on the widget will now call ``on_activate()``.
- ``widgets.CycleHotkeyLabel``: clicking on the widget will now cycle the options and trigger ``on_change()``. This also applies to the ``ToggleHotkeyLabel`` subclass.
# 0.47.05-r6

@ -154,11 +154,20 @@ function numberList(arg, arg_name, list_length)
return strings
end
-- throws if val is not a nonnegative integer; otherwise returns val
local function check_nonnegative_int(val, arg_name)
function positiveInt(arg, arg_name)
local val = tonumber(arg)
if not val or val <= 0 or val ~= math.floor(val) then
arg_error(arg_name,
'expected positive integer; got "%s"', tostring(arg))
end
return val
end
function nonnegativeInt(arg, arg_name)
local val = tonumber(arg)
if not val or val < 0 or val ~= math.floor(val) then
arg_error(arg_name,
'expected non-negative integer; got "%s"', tostring(val))
'expected non-negative integer; got "%s"', tostring(arg))
end
return val
end
@ -177,9 +186,9 @@ function coords(arg, arg_name, skip_validation)
return cursor
end
local numbers = numberList(arg, arg_name, 3)
local pos = xyz2pos(check_nonnegative_int(numbers[1]),
check_nonnegative_int(numbers[2]),
check_nonnegative_int(numbers[3]))
local pos = xyz2pos(nonnegativeInt(numbers[1]),
nonnegativeInt(numbers[2]),
nonnegativeInt(numbers[3]))
if not skip_validation and not dfhack.maps.isValidTilePos(pos) then
arg_error(arg_name,
'specified coordinates not on current map: "%s"', arg)

@ -307,10 +307,10 @@ function EditField:onInput(keys)
elseif keys.CURSOR_LEFT then
self:setCursor(self.cursor - 1)
return true
elseif keys.A_MOVE_W_DOWN then -- Ctrl-Left (prev word start)
local _, prev_word_start = self.text:sub(1, self.cursor-1):
find('.*[^%w_%-]+[%w_%-]')
self:setCursor(prev_word_start or 1)
elseif keys.A_MOVE_W_DOWN then -- Ctrl-Left (end of prev word)
local _, prev_word_end = self.text:sub(1, self.cursor-1):
find('.*[%w_%-][^%w_%-]')
self:setCursor(prev_word_end or 1)
return true
elseif keys.A_CARE_MOVE_W then -- Alt-Left (home)
self:setCursor(1)
@ -318,9 +318,9 @@ function EditField:onInput(keys)
elseif keys.CURSOR_RIGHT then
self:setCursor(self.cursor + 1)
return true
elseif keys.A_MOVE_E_DOWN then -- Ctrl-Right (next word end)
local _, next_word_end = self.text:find('[%w_%-]+[^%w_%-]', self.cursor)
self:setCursor(next_word_end)
elseif keys.A_MOVE_E_DOWN then -- Ctrl-Right (beginning of next word)
local _,next_word_start = self.text:find('[^%w_%-][%w_%-]', self.cursor)
self:setCursor(next_word_start)
return true
elseif keys.A_CARE_MOVE_E then -- Alt-Right (end)
self:setCursor()
@ -763,7 +763,6 @@ function HotkeyLabel:onInput(keys)
self.on_activate()
return true
end
end
----------------------
@ -838,6 +837,15 @@ function CycleHotkeyLabel:getOptionValue(option_idx)
return option
end
function CycleHotkeyLabel:onInput(keys)
if CycleHotkeyLabel.super.onInput(self, keys) then
return true
elseif keys._MOUSE_L and self:getMousePos() then
self:cycle()
return true
end
end
-----------------------
-- ToggleHotkeyLabel --
-----------------------
@ -1154,7 +1162,7 @@ end
function FilteredList:setChoices(choices, pos)
choices = choices or {}
self.edit.text = ''
self.edit:setText('')
self.list:setChoices(choices, pos)
self.choices = self.list.choices
self.not_found.visible = (#choices == 0)
@ -1196,7 +1204,7 @@ function FilteredList:setFilter(filter, pos)
local cidx = nil
filter = filter or ''
self.edit.text = filter
self.edit:setText(filter)
if filter ~= '' then
local tokens = filter:split()

@ -4,9 +4,9 @@
-- text exists, it is read from the script sources (for scripts) or the string
-- passed to the PluginCommand initializer (for plugins).
--
-- For plugins that don't register a command with the same name as the plugin,
-- the plugin name is registered as a separate entry so documentation on what
-- happens when you enable the plugin can be found.
-- There should be one help file for each plugin that contains a summary for the
-- plugin itself and help for all the commands that plugin provides (if any).
-- Each script should also have one documentation file.
--
-- The database is lazy-loaded when an API method is called. It rechecks its
-- help sources for updates if an API method has not been called in the last
@ -14,6 +14,8 @@
local _ENV = mkmodule('helpdb')
local MAX_STALE_MS = 60000
-- paths
local RENDERED_PATH = 'hack/docs/docs/tools/'
local TAG_DEFINITIONS = 'hack/docs/docs/Tags.txt'
@ -80,7 +82,7 @@ local BUILTINS = {
-- source_timestamp (mtime, 0 for non-files),
-- source_path (string, nil for non-files)
-- }
textdb = textdb or {}
local textdb = {}
-- entry database, points to text in textdb
-- entry name -> {
@ -90,13 +92,13 @@ textdb = textdb or {}
-- }
--
-- entry_types is a set because plugin commands can also be the plugin names.
entrydb = entrydb or {}
local entrydb = {}
-- tag name -> list of entry names
-- Tags defined in the TAG_DEFINITIONS file that have no associated db entries
-- will have an empty list.
tag_index = tag_index or {}
local tag_index = {}
---------------------------------------------------------------------------
-- data ingestion
@ -146,7 +148,7 @@ local function update_entry(entry, iterator, opts)
local first_line_is_short_help = opts.first_line_is_short_help
local begin_marker_found,header_found = not opts.begin_marker,opts.no_header
local tags_found, short_help_found = false, opts.skip_short_help
local in_tags, in_keybinding, in_short_help = false, false, false
local in_tags, in_short_help = false, false
for line in iterator do
if not short_help_found and first_line_is_short_help then
line = line:trim()
@ -188,10 +190,6 @@ local function update_entry(entry, iterator, opts)
elseif not tags_found and line:find('^[*]*Tags:[*]*') then
_,_,tags = line:trim():find('[*]*Tags:[*]* *(.*)')
in_tags, tags_found = true, true
elseif in_keybinding then
if #line == 0 then in_keybinding = false end
elseif line:find('^[*]*Keybinding:') then
in_keybinding = true
elseif not short_help_found and
line:find('^%w') then
if in_short_help then
@ -212,7 +210,9 @@ local function update_entry(entry, iterator, opts)
end
entry.tags = {}
for _,tag in ipairs(tags:split('[ ,|]+')) do
entry.tags[tag] = true
if #tag > 0 and tag_index[tag] then
entry.tags[tag] = true
end
end
if #lines > 0 then
entry.long_help = table.concat(lines, '\n')
@ -230,7 +230,11 @@ local function make_rendered_entry(old_entry, entry_name, kwargs)
end
kwargs.source_path, kwargs.source_timestamp = source_path, source_timestamp
local entry = make_default_entry(entry_name, HELP_SOURCES.RENDERED, kwargs)
update_entry(entry, io.lines(source_path))
local ok, lines = pcall(io.lines, source_path)
if not ok then
return entry
end
update_entry(entry, lines)
return entry
end
@ -411,11 +415,11 @@ local function index_tags()
end
-- ensures the db is up to date by scanning all help sources. does not do
-- anything if it has already been run within the last 60 seconds.
last_refresh_ms = last_refresh_ms or 0
-- anything if it has already been run within the last MAX_STALE_MS milliseconds
local last_refresh_ms = 0
local function ensure_db()
local now_ms = dfhack.getTickCount()
if now_ms - last_refresh_ms < 60000 then return end
if now_ms - last_refresh_ms <= MAX_STALE_MS then return end
last_refresh_ms = now_ms
local old_db = textdb
@ -445,7 +449,6 @@ local function has_keys(str, dict)
if not str or #str == 0 then
return false
end
ensure_db()
for _,s in ipairs(normalize_string_list(str)) do
if not dict[s] then
return false
@ -454,8 +457,10 @@ local function has_keys(str, dict)
return true
end
-- returns whether the given string (or list of strings) is an entry in the db
-- returns whether the given string (or list of strings) is an entry (are all
-- entries) in the db
function is_entry(str)
ensure_db()
return has_keys(str, entrydb)
end
@ -482,6 +487,17 @@ function get_entry_long_help(entry)
return get_db_property(entry, 'long_help')
end
-- returns the set of tags associated with the entry
function get_entry_tags(entry)
return get_db_property(entry, 'tags')
end
-- returns whether the given string (or list of strings) matches a tag name
function is_tag(str)
ensure_db()
return has_keys(str, tag_index)
end
local function set_to_sorted_list(set)
local list = {}
for item in pairs(set) do
@ -491,16 +507,6 @@ local function set_to_sorted_list(set)
return list
end
-- returns the list of tags associated with the entry, in alphabetical order
function get_entry_tags(entry)
return set_to_sorted_list(get_db_property(entry, 'tags'))
end
-- returns whether the given string (or list of strings) matches a tag name
function is_tag(str)
return has_keys(str, tag_index)
end
-- returns the defined tags in alphabetical order
function get_tags()
ensure_db()
@ -510,8 +516,7 @@ end
function get_tag_data(tag)
ensure_db()
if not tag_index[tag] then
dfhack.printerr('invalid tag: ' .. tag)
return {}
error(('helpdb tag not found: "%s"'):format(tag))
end
return tag_index[tag]
end
@ -533,7 +538,7 @@ end
-- sorts by last path component, then by parent path components.
-- something comes before nothing.
-- e.g. gui/autofarm comes immediately before autofarm
local function sort_by_basename(a, b)
function sort_by_basename(a, b)
local a = chunk_for_sorting(a)
local b = chunk_for_sorting(b)
local i = 1
@ -563,10 +568,10 @@ local function matches(entry_name, filter)
return false
end
end
if filter.types then
if filter.entry_type then
local matched = false
local etypes = get_db_property(entry_name, 'entry_types')
for _,etype in ipairs(filter.types) do
for _,etype in ipairs(filter.entry_type) do
if etypes[etype] then
matched = true
break
@ -598,8 +603,8 @@ local function normalize_filter(f)
local filter = {}
filter.str = normalize_string_list(f.str)
filter.tag = normalize_string_list(f.tag)
filter.types = normalize_string_list(f.types)
if not filter.str and not filter.tag and not filter.types then
filter.entry_type = normalize_string_list(f.entry_type)
if not filter.str and not filter.tag and not filter.entry_type then
return nil
end
return filter
@ -614,11 +619,11 @@ end
-- includes entry names that match any of the given substrings.
-- tag - if a string, filters by the given tag name. if a table of strings,
-- includes entries that match any of the given tags.
-- types - if a string, matches entries of the given type. if a table of
-- strings, includes entries that match any of the given types. valid
-- types are: "builtin", "plugin", "command". note that many plugin
-- commands have the same name as the plugin, so those entries will
-- match both "plugin" and "command" types.
-- entry_type - if a string, matches entries of the given type. if a table of
-- strings, includes entries that match any of the given types. valid
-- types are: "builtin", "plugin", "command". note that many plugin
-- commands have the same name as the plugin, so those entries will
-- match both "plugin" and "command" types.
function search_entries(include, exclude)
ensure_db()
include = normalize_filter(include)
@ -636,7 +641,7 @@ end
-- returns a list of all commands. used by Core's autocomplete functionality.
function get_commands()
local include = {types={ENTRY_TYPES.COMMAND}}
local include = {entry_type=ENTRY_TYPES.COMMAND}
return search_entries(include)
end
@ -692,12 +697,12 @@ end
-- prints the requested entries to the console. include and exclude filters are
-- defined as in search_entries() above.
function list_entries(skip_tags, include, exclude)
local function list_entries(skip_tags, include, exclude)
local entries = search_entries(include, exclude)
for _,entry in ipairs(entries) do
print_columns(entry, get_entry_short_help(entry))
if not skip_tags then
local tags = get_entry_tags(entry)
local tags = set_to_sorted_list(get_entry_tags(entry))
if #tags > 0 then
print((' tags: %s'):format(table.concat(tags, ', ')))
end
@ -717,14 +722,14 @@ end
-- devel/ directories. otherwise those scripts will be
-- excluded
function ls(filter_str, skip_tags, show_dev_commands)
local include = {types={ENTRY_TYPES.COMMAND}}
local include = {entry_type={ENTRY_TYPES.COMMAND}}
if is_tag(filter_str) then
include.tag = filter_str
else
include.str = filter_str
end
list_entries(skip_tags, include,
show_dev_commands and {} or {str={'modtools/', 'devel/'}})
show_dev_commands and {} or {tag='dev'})
end
return _ENV

@ -186,7 +186,7 @@ endif()
# this is the skeleton plugin. If you want to make your own, make a copy and then change it
option(BUILD_SKELETON "Build the skeleton plugin." OFF)
if(BUILD_SKELETON)
add_subdirectory(skeleton)
dfhack_plugin(skeleton examples/skeleton.cpp)
endif()
macro(subdirlist result subdir)

@ -0,0 +1,166 @@
// This template is appropriate for plugins that periodically check game state
// and make some sort of automated change. These types of plugins typically
// provide a command that can be used to configure the plugin behavior and
// require a world to be loaded before they can function. This kind of plugin
// should persist its state in the savegame and auto-re-enable itself when a
// savegame that had this plugin enabled is loaded.
#include <string>
#include <vector>
#include "df/world.h"
#include "Core.h"
#include "Debug.h"
#include "PluginManager.h"
#include "modules/Persistence.h"
#include "modules/World.h"
using std::string;
using std::vector;
using namespace DFHack;
DFHACK_PLUGIN("persistent_per_save_example");
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
REQUIRE_GLOBAL(world);
// logging levels can be dynamically controlled with the `debugfilter` command.
namespace DFHack {
// for configuration-related logging
DBG_DECLARE(persistent_per_save_example, status, DebugCategory::LINFO);
// for logging during the periodic scan
DBG_DECLARE(persistent_per_save_example, cycle, DebugCategory::LINFO);
}
static const string CONFIG_KEY = string(plugin_name) + "/config";
static PersistentDataItem config;
enum ConfigValues {
CONFIG_IS_ENABLED = 0,
CONFIG_CYCLE_TICKS = 1,
};
static int get_config_val(int index) {
if (!config.isValid())
return -1;
return config.ival(index);
}
static bool get_config_bool(int index) {
return get_config_val(index) == 1;
}
static void set_config_val(int index, int value) {
if (config.isValid())
config.ival(index) = value;
}
static void set_config_bool(int index, bool value) {
set_config_val(index, value ? 1 : 0);
}
static int32_t cycle_timestamp = 0; // world->frame_counter at last cycle
static command_result do_command(color_ostream &out, vector<string> &parameters);
static void do_cycle(color_ostream &out);
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
DEBUG(status,out).print("initializing %s\n", plugin_name);
// provide a configuration interface for the plugin
commands.push_back(PluginCommand(
plugin_name,
"Short (~54 character) description of command.",
do_command));
return CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
if (!Core::getInstance().isWorldLoaded()) {
out.printerr("Cannot enable %s without a loaded world.\n", plugin_name);
return CR_FAILURE;
}
if (enable != is_enabled) {
is_enabled = enable;
DEBUG(status,out).print("%s from the API; persisting\n",
is_enabled ? "enabled" : "disabled");
set_config_bool(CONFIG_IS_ENABLED, is_enabled);
} else {
DEBUG(status,out).print("%s from the API, but already %s; no action\n",
is_enabled ? "enabled" : "disabled",
is_enabled ? "enabled" : "disabled");
}
return CR_OK;
}
DFhackCExport command_result plugin_shutdown (color_ostream &out) {
DEBUG(status,out).print("shutting down %s\n", plugin_name);
return CR_OK;
}
DFhackCExport command_result plugin_load_data (color_ostream &out) {
config = World::GetPersistentData(CONFIG_KEY);
if (!config.isValid()) {
DEBUG(status,out).print("no config found in this save; initializing\n");
config = World::AddPersistentData(CONFIG_KEY);
set_config_bool(CONFIG_IS_ENABLED, is_enabled);
set_config_val(CONFIG_CYCLE_TICKS, 6000);
}
// we have to copy our enabled flag into the global plugin variable, but
// all the other state we can directly read/modify from the persistent
// data structure.
is_enabled = get_config_bool(CONFIG_IS_ENABLED);
DEBUG(status,out).print("loading persisted enabled state: %s\n",
is_enabled ? "true" : "false");
return CR_OK;
}
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
if (event == DFHack::SC_WORLD_UNLOADED) {
if (is_enabled) {
DEBUG(status,out).print("world unloaded; disabling %s\n",
plugin_name);
is_enabled = false;
}
}
return CR_OK;
}
DFhackCExport command_result plugin_onupdate(color_ostream &out) {
if (is_enabled && world->frame_counter - cycle_timestamp >= get_config_val(CONFIG_CYCLE_TICKS))
do_cycle(out);
return CR_OK;
}
static command_result do_command(color_ostream &out, vector<string> &parameters) {
// be sure to suspend the core if any DF state is read or modified
CoreSuspender suspend;
if (!Core::getInstance().isWorldLoaded()) {
out.printerr("Cannot run %s without a loaded world.\n", plugin_name);
return CR_FAILURE;
}
// TODO: configuration logic
// simple commandline parsing can be done in C++, but there are lua libraries
// that can easily handle more complex commandlines. see the blueprint plugin
// for an example.
return CR_OK;
}
/////////////////////////////////////////////////////
// cycle logic
//
static void do_cycle(color_ostream &out) {
// mark that we have recently run
cycle_timestamp = world->frame_counter;
DEBUG(cycle,out).print("running %s cycle\n", plugin_name);
// TODO: logic that runs every get_config_val(CONFIG_CYCLE_TICKS) ticks
}

@ -0,0 +1,41 @@
// This template is appropriate for plugins that simply provide one or more
// commands, but don't need to be "enabled" to function.
#include <string>
#include <vector>
#include "Debug.h"
#include "PluginManager.h"
using std::string;
using std::vector;
using namespace DFHack;
DFHACK_PLUGIN("simple_command_example");
namespace DFHack {
DBG_DECLARE(simple_command_example, log);
}
static command_result do_command(color_ostream &out, vector<string> &parameters);
DFhackCExport command_result plugin_init(color_ostream &out, std::vector <PluginCommand> &commands) {
DEBUG(log,out).print("initializing %s\n", plugin_name);
commands.push_back(PluginCommand(
plugin_name,
"Short (~54 character) description of command.",
do_command));
return CR_OK;
}
static command_result do_command(color_ostream &out, vector<string> &parameters) {
// be sure to suspend the core if any DF state is read or modified
CoreSuspender suspend;
// TODO: command logic
return CR_OK;
}

@ -0,0 +1,193 @@
// This is an example plugin that documents and implements all the plugin
// callbacks and features. You can include it in the regular build by setting
// the BUILD_SKELETON option in CMake to ON. Play with loading and unloading
// the plugin in various game states (e.g. with and without a world loaded),
// and see the debug messages get printed to the console.
//
// See the other example plugins in this directory for plugins that are
// configured for specific use cases (but don't come with as many comments as
// this one does).
#include <string>
#include <vector>
#include "df/world.h"
#include "Core.h"
#include "Debug.h"
#include "PluginManager.h"
#include "modules/Persistence.h"
#include "modules/World.h"
using std::string;
using std::vector;
using namespace DFHack;
// Expose the plugin name to the DFHack core, as well as metadata like the
// DFHack version that this plugin was compiled with. This macro provides a
// variable for the plugin name as const char * plugin_name.
// The name provided must correspond to the filename --
// skeleton.plug.so, skeleton.plug.dylib, or skeleton.plug.dll in this case
DFHACK_PLUGIN("skeleton");
// The identifier declared with this macro (i.e. is_enabled) is used to track
// whether the plugin is in an "enabled" state. If you don't need enablement
// for your plugin, you don't need this line. This variable will also be read
// by the `plug` builtin command; when true the plugin will be shown as enabled.
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
// Any globals a plugin requires (e.g. world) should be listed here.
// For example, this line expands to "using df::global::world" and prevents the
// plugin from being loaded if df::global::world is null (i.e. missing from
// symbols.xml).
REQUIRE_GLOBAL(world);
// logging levels can be dynamically controlled with the `debugfilter` command.
// Actual plugins will likely want to set the default level to LINFO or LWARNING
// instead of the LDEBUG used here.
namespace DFHack {
// for configuration-related logging
DBG_DECLARE(skeleton, status, DebugCategory::LDEBUG);
// for plugin_onupdate logging
DBG_DECLARE(skeleton, onupdate, DebugCategory::LDEBUG);
// for command-related logging
DBG_DECLARE(skeleton, command, DebugCategory::LDEBUG);
}
static command_result command_callback1(color_ostream &out, vector<string> &parameters);
// run when the plugin is loaded
DFhackCExport command_result plugin_init(color_ostream &out, std::vector<PluginCommand> &commands) {
DEBUG(status,out).print("initializing %s\n", plugin_name);
// For in-tree plugins, don't use the "usage" parameter of PluginCommand.
// Instead, add an .rst file with the same name as the plugin to the
// docs/plugins/ directory.
commands.push_back(PluginCommand(
"skeleton",
"Short (~54 character) description of command.", // to use one line in the ``[DFHack]# ls`` output
command_callback1));
return CR_OK;
}
// run when the plugin is unloaded
DFhackCExport command_result plugin_shutdown(color_ostream &out) {
DEBUG(status,out).print("shutting down %s\n", plugin_name);
// You *MUST* kill all threads you created before this returns.
// If everything fails, just return CR_FAILURE. Your plugin will be
// in a zombie state, but things won't crash.
return CR_OK;
}
// run when the `enable` or `disable` command is run with this plugin name as
// an argument
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
DEBUG(status,out).print("%s from the API\n", enable ? "enabled" : "disabled");
// you have to maintain the state of the is_enabled variable yourself. it
// doesn't happen automatically.
is_enabled = enable;
return CR_OK;
}
// Called to notify the plugin about important state changes.
// Invoked with DF suspended, and always before the matching plugin_onupdate.
// More event codes may be added in the future.
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
switch (event) {
case SC_UNKNOWN:
DEBUG(status,out).print("game state changed: SC_UNKNOWN\n");
break;
case SC_WORLD_LOADED:
DEBUG(status,out).print("game state changed: SC_WORLD_LOADED\n");
break;
case SC_WORLD_UNLOADED:
DEBUG(status,out).print("game state changed: SC_WORLD_UNLOADED\n");
break;
case SC_MAP_LOADED:
DEBUG(status,out).print("game state changed: SC_MAP_LOADED\n");
break;
case SC_MAP_UNLOADED:
DEBUG(status,out).print("game state changed: SC_MAP_UNLOADED\n");
break;
case SC_VIEWSCREEN_CHANGED:
DEBUG(status,out).print("game state changed: SC_VIEWSCREEN_CHANGED\n");
break;
case SC_CORE_INITIALIZED:
DEBUG(status,out).print("game state changed: SC_CORE_INITIALIZED\n");
break;
case SC_BEGIN_UNLOAD:
DEBUG(status,out).print("game state changed: SC_BEGIN_UNLOAD\n");
break;
case SC_PAUSED:
DEBUG(status,out).print("game state changed: SC_PAUSED\n");
break;
case SC_UNPAUSED:
DEBUG(status,out).print("game state changed: SC_UNPAUSED\n");
break;
}
return CR_OK;
}
// Whatever you put here will be done in each game frame refresh. Don't abuse it.
// Note that if the plugin implements the enabled API, this function is only called
// if the plugin is enabled.
DFhackCExport command_result plugin_onupdate (color_ostream &out) {
DEBUG(onupdate,out).print(
"onupdate called (run 'debugfilter set info skeleton onupdate' to stop"
" seeing these messages)\n");
return CR_OK;
}
// If you need to save or load world-specific data, define these functions.
// plugin_save_data is called when the game might be about to save the world,
// and plugin_load_data is called whenever a new world is loaded. If the plugin
// is loaded or unloaded while a world is active, plugin_save_data or
// plugin_load_data will be called immediately.
DFhackCExport command_result plugin_save_data (color_ostream &out) {
DEBUG(status,out).print("save or unload is imminent; time to persist state\n");
// Call functions in the Persistence module here. If your PersistantDataItem
// objects are already up to date, then they will get persisted with the
// save automatically and there is nothing extra you need to do here.
return CR_OK;
}
DFhackCExport command_result plugin_load_data (color_ostream &out) {
DEBUG(status,out).print("world is loading; time to load persisted state\n");
// Call functions in the Persistence module here. See
// persistent_per_save_example.cpp for an example.
return CR_OK;
}
// This is the callback we registered in plugin_init. Note that while plugin
// callbacks are called with the core suspended, command callbacks are called
// from a different thread and need to explicity suspend the core if they
// interact with Lua or DF game state (most commands do at least one of these).
static command_result command_callback1(color_ostream &out, vector<string> &parameters) {
DEBUG(command,out).print("%s command called with %zu parameters\n",
plugin_name, parameters.size());
// I'll say it again: always suspend the core in command callbacks unless
// all your data is local.
CoreSuspender suspend;
// Return CR_WRONG_USAGE to print out your help text. The help text is
// sourced from the associated rst file in docs/plugins/. The same help will
// also be returned by 'help your-command'.
// simple commandline parsing can be done in C++, but there are lua libraries
// that can easily handle more complex commandlines. see the blueprint plugin
// for an example.
// TODO: do something according to the flags set in the options struct
return CR_OK;
}

@ -0,0 +1,57 @@
// This template is appropriate for plugins that can be enabled to make some
// specific persistent change to the game, but don't need a world to be loaded
// before they are enabled. These types of plugins typically register some sort
// of hook on enable and clear the hook on disable. They are generally enabled
// from dfhack.init and do not need to persist and reload their enabled state.
#include <string>
#include <vector>
#include "df/viewscreen_titlest.h"
#include "Debug.h"
#include "PluginManager.h"
#include "VTableInterpose.h"
using std::string;
using std::vector;
using namespace DFHack;
DFHACK_PLUGIN("ui_addition_example");
DFHACK_PLUGIN_IS_ENABLED(is_enabled);
namespace DFHack {
DBG_DECLARE(ui_addition_example, log);
}
// example of hooking a screen so the plugin code will run whenever the screen
// is visible
struct title_version_hook : df::viewscreen_titlest {
typedef df::viewscreen_titlest interpose_base;
DEFINE_VMETHOD_INTERPOSE(void, render, ()) {
INTERPOSE_NEXT(render)();
// TODO: injected render logic here
}
};
IMPLEMENT_VMETHOD_INTERPOSE(title_version_hook, render);
DFhackCExport command_result plugin_shutdown (color_ostream &out) {
DEBUG(log,out).print("shutting down %s\n", plugin_name);
INTERPOSE_HOOK(title_version_hook, render).remove();
return CR_OK;
}
DFhackCExport command_result plugin_enable (color_ostream &out, bool enable) {
if (enable != is_enabled) {
DEBUG(log,out).print("%s %s\n", plugin_name,
is_enabled ? "enabled" : "disabled");
if (!INTERPOSE_HOOK(title_version_hook, render).apply(enable))
return CR_FAILURE;
is_enabled = enable;
}
return CR_OK;
}

@ -91,7 +91,9 @@ command_result df_seedwatch(color_ostream &out, vector<string>& parameters)
map<string, int32_t> plantIDs;
for(size_t i = 0; i < world->raws.plants.all.size(); ++i)
{
plantIDs[world->raws.plants.all[i]->id] = i;
auto & plant = world->raws.plants.all[i];
if (plant->material_defs.type[plant_material_def::seed] != -1)
plantIDs[plant->id] = i;
}
t_gamemodes gm;
@ -182,10 +184,8 @@ command_result df_seedwatch(color_ostream &out, vector<string>& parameters)
if(limit < 0) limit = 0;
if(parameters[0] == "all")
{
for(auto i = abbreviations.begin(); i != abbreviations.end(); ++i)
{
if(plantIDs.count(i->second) > 0) Kitchen::setLimit(plantIDs[i->second], limit);
}
for(auto & entry : plantIDs)
Kitchen::setLimit(entry.second, limit);
}
else
{

@ -1,36 +0,0 @@
project(skeleton)
# A list of source files
set(PROJECT_SRCS
skeleton.cpp
)
# A list of headers
set(PROJECT_HDRS
skeleton.h
)
set_source_files_properties(${PROJECT_HDRS} PROPERTIES HEADER_FILE_ONLY TRUE)
# mash them together (headers are marked as headers and nothing will try to compile them)
list(APPEND PROJECT_SRCS ${PROJECT_HDRS})
# option to use a thread for no particular reason
option(SKELETON_THREAD "Use threads in the skeleton plugin." ON)
if(UNIX)
if(APPLE)
set(PROJECT_LIBS
# add any extra mac libraries here
${PROJECT_LIBS}
)
else()
set(PROJECT_LIBS
# add any extra linux libraries here
${PROJECT_LIBS}
)
endif()
else()
set(PROJECT_LIBS
# add any extra windows libraries here
${PROJECT_LIBS}
)
endif()
# this makes sure all the stuff is put in proper places and linked to dfhack
dfhack_plugin(skeleton ${PROJECT_SRCS} LINK_LIBRARIES ${PROJECT_LIBS})

@ -1,171 +0,0 @@
// This is a generic plugin that does nothing useful apart from acting as an example... of a plugin that does nothing :D
// some headers required for a plugin. Nothing special, just the basics.
#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <modules/EventManager.h>
// If you need to save data per-world:
//#include "modules/Persistence.h"
// DF data structure definition headers
#include "DataDefs.h"
//#include "df/world.h"
// our own, empty header.
#include "skeleton.h"
using namespace DFHack;
using namespace df::enums;
// Expose the plugin name to the DFHack core, as well as metadata like the DFHack version.
// The name string provided must correspond to the filename -
// skeleton.plug.so, skeleton.plug.dylib, or skeleton.plug.dll in this case
DFHACK_PLUGIN("skeleton");
// The identifier declared with this macro (ie. enabled) can be specified by the user
// and subsequently used to manage the plugin's operations.
// This will also be tracked by `plug`; when true the plugin will be shown as enabled.
DFHACK_PLUGIN_IS_ENABLED(enabled);
// Any globals a plugin requires (e.g. world) should be listed here.
// For example, this line expands to "using df::global::world" and prevents the
// plugin from being loaded if df::global::world is null (i.e. missing from symbols.xml):
//
REQUIRE_GLOBAL(world);
// You may want some compile time debugging options
// one easy system just requires you to cache the color_ostream &out into a global debug variable
//#define P_DEBUG 1
//uint16_t maxTickFreq = 1200; //maybe you want to use some events
command_result command_callback1(color_ostream &out, std::vector<std::string> &parameters);
DFhackCExport command_result plugin_init(color_ostream &out, std::vector<PluginCommand> &commands) {
commands.push_back(PluginCommand("skeleton",
"~54 character description of plugin", //to use one line in the ``[DFHack]# ls`` output
command_callback1,
false,
"example usage"
" skeleton <option> <args>\n"
" explanation of plugin/command\n"
"\n"
" skeleton\n"
" what happens when using the command\n"
"\n"
" skeleton option1\n"
" what happens when using the command with option1\n"
"\n"));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown(color_ostream &out) {
// You *MUST* kill all threads you created before this returns.
// If everything fails, just return CR_FAILURE. Your plugin will be
// in a zombie state, but things won't crash.
return CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
namespace EM = EventManager;
if (enable && !enabled) {
//using namespace EM::EventType;
//EM::EventHandler eventHandler(onNewEvent, maxTickFreq);
//EM::registerListener(EventType::JOB_COMPLETED, eventHandler, plugin_self);
//out.print("plugin enabled!\n");
} else if (!enable && enabled) {
EM::unregisterAll(plugin_self);
//out.print("plugin disabled!\n");
}
enabled = enable;
return CR_OK;
}
/* OPTIONAL *
// Called to notify the plugin about important state changes.
// Invoked with DF suspended, and always before the matching plugin_onupdate.
// More event codes may be added in the future.
DFhackCExport command_result plugin_onstatechange(color_ostream &out, state_change_event event) {
if (enabled) {
switch (event) {
case SC_UNKNOWN:
break;
case SC_WORLD_LOADED:
break;
case SC_WORLD_UNLOADED:
break;
case SC_MAP_LOADED:
break;
case SC_MAP_UNLOADED:
break;
case SC_VIEWSCREEN_CHANGED:
break;
case SC_CORE_INITIALIZED:
break;
case SC_BEGIN_UNLOAD:
break;
case SC_PAUSED:
break;
case SC_UNPAUSED:
break;
}
}
return CR_OK;
}
// Whatever you put here will be done in each game step. Don't abuse it.
DFhackCExport command_result plugin_onupdate ( color_ostream &out )
{
// whetever. You don't need to suspend DF execution here.
return CR_OK;
}
// If you need to save or load world-specific data, define these functions.
// plugin_save_data is called when the game might be about to save the world,
// and plugin_load_data is called whenever a new world is loaded. If the plugin
// is loaded or unloaded while a world is active, plugin_save_data or
// plugin_load_data will be called immediately.
DFhackCExport command_result plugin_save_data (color_ostream &out)
{
// Call functions in the Persistence module here.
return CR_OK;
}
DFhackCExport command_result plugin_load_data (color_ostream &out)
{
// Call functions in the Persistence module here.
return CR_OK;
}
* OPTIONAL */
// A command! It sits around and looks pretty. And it's nice and friendly.
command_result command_callback1(color_ostream &out, std::vector<std::string> &parameters) {
// It's nice to print a help message you get invalid options
// from the user instead of just acting strange.
// This can be achieved by adding the extended help string to the
// PluginCommand registration as show above, and then returning
// CR_WRONG_USAGE from the function. The same string will also
// be used by 'help your-command'.
if (!parameters.empty()) {
return CR_WRONG_USAGE; //or maybe you want it to do something else
}
// Commands are called from threads other than the DF one.
// Suspend this thread until DF has time for us.
// **If you use CoreSuspender** it'll automatically resume DF when
// execution leaves the current scope.
CoreSuspender suspend;
// Actually do something here. Yay.
// process parameters
if (parameters.size() == 1 && parameters[0] == "option1") {
// stuff
} else {
return CR_FAILURE;
}
// Give control back to DF.
return CR_OK;
}

@ -1,61 +0,0 @@
#include "Core.h"
#include <Console.h>
#include <Export.h>
#include <PluginManager.h>
#include <modules/EventManager.h>
//#include "df/world.h"
using namespace DFHack;
using namespace df::enums;
DFHACK_PLUGIN("skeleton2");
DFHACK_PLUGIN_IS_ENABLED(enabled);
//REQUIRE_GLOBAL(world);
command_result skeleton2 (color_ostream &out, std::vector <std::string> & parameters);
DFhackCExport command_result plugin_init (color_ostream &out, std::vector <PluginCommand> &commands) {
commands.push_back(PluginCommand("skeleton2",
"~54 character description of plugin", //to use one line in the ``[DFHack]# ls`` output
skeleton2,
false,
"example usage"
" skeleton2 <option> <args>\n"
" explanation of plugin/command\n"
"\n"
" skeleton2\n"
" what happens when using the command\n"
"\n"
" skeleton2 option1\n"
" what happens when using the command with option1\n"
"\n"));
return CR_OK;
}
DFhackCExport command_result plugin_shutdown (color_ostream &out) {
return CR_OK;
}
DFhackCExport command_result plugin_enable(color_ostream &out, bool enable) {
namespace EM = EventManager;
if (enable && !enabled) {
//using namespace EM::EventType;
//EM::EventHandler eventHandler(onNewEvent, maxTickFreq);
//EM::registerListener(EventType::JOB_COMPLETED, eventHandler, plugin_self);
//out.print("plugin enabled!\n");
} else if (!enable && enabled) {
EM::unregisterAll(plugin_self);
//out.print("plugin disabled!\n");
}
enabled = enable;
return CR_OK;
}
command_result skeleton2 (color_ostream &out, std::vector <std::string> & parameters) {
if (!parameters.empty())
return CR_WRONG_USAGE;
CoreSuspender suspend;
out.print("blah");
return CR_OK;
}

@ -1 +1 @@
Subproject commit 3d65c9a75a8c7a3607e71529f6264bab3e637755
Subproject commit 52e292ecf7e498c6114a81223380d2b12f3718c1

@ -265,3 +265,33 @@ function test.coords()
end)
end)
end
function test.positiveInt()
expect.eq(5, argparse.positiveInt(5))
expect.eq(5, argparse.positiveInt('5'))
expect.eq(5, argparse.positiveInt('5.0'))
expect.eq(1, argparse.positiveInt('1'))
expect.error_match('expected positive integer',
function() argparse.positiveInt('0') end)
expect.error_match('expected positive integer',
function() argparse.positiveInt('5.01') end)
expect.error_match('expected positive integer',
function() argparse.positiveInt(-1) end)
end
function test.nonnegativeInt()
expect.eq(5, argparse.nonnegativeInt(5))
expect.eq(5, argparse.nonnegativeInt('5'))
expect.eq(5, argparse.nonnegativeInt('5.0'))
expect.eq(1, argparse.nonnegativeInt('1'))
expect.eq(0, argparse.nonnegativeInt('0'))
expect.eq(0, argparse.nonnegativeInt('-0'))
expect.error_match('expected non%-negative integer',
function() argparse.nonnegativeInt('-0.01') end)
expect.error_match('expected non%-negative integer',
function() argparse.nonnegativeInt(-5) end)
expect.error_match('expected non%-negative integer',
function() argparse.nonnegativeInt(-1) end)
end

@ -0,0 +1,56 @@
local widgets = require('gui.widgets')
function test.editfield_cursor()
local e = widgets.EditField{}
e:setFocus(true)
expect.eq(1, e.cursor, 'cursor should be after the empty string')
e:onInput{_STRING=string.byte('a')}
expect.eq('a', e.text)
expect.eq(2, e.cursor)
e:setText('one two three')
expect.eq(14, e.cursor, 'cursor should be after the last char')
e:onInput{_STRING=string.byte('s')}
expect.eq('one two threes', e.text)
expect.eq(15, e.cursor)
e:setCursor(4)
e:onInput{_STRING=string.byte('s')}
expect.eq('ones two threes', e.text)
expect.eq(5, e.cursor)
e:onInput{CURSOR_LEFT=true}
expect.eq(4, e.cursor)
e:onInput{CURSOR_RIGHT=true}
expect.eq(5, e.cursor)
e:onInput{A_CARE_MOVE_W=true}
expect.eq(1, e.cursor, 'interpret alt-left as home')
e:onInput{A_MOVE_E_DOWN=true}
expect.eq(6, e.cursor, 'interpret ctrl-right as goto beginning of next word')
e:onInput{A_CARE_MOVE_E=true}
expect.eq(16, e.cursor, 'interpret alt-right as end')
e:onInput{A_MOVE_W_DOWN=true}
expect.eq(9, e.cursor, 'interpret ctrl-left as goto end of previous word')
end
function test.editfield_click()
local e = widgets.EditField{text='word'}
e:setFocus(true)
expect.eq(5, e.cursor)
mock.patch(e, 'getMousePos', mock.func(0), function()
e:onInput{_MOUSE_L=true}
expect.eq(1, e.cursor)
end)
mock.patch(e, 'getMousePos', mock.func(20), function()
e:onInput{_MOUSE_L=true}
expect.eq(5, e.cursor, 'should only seek to end of text')
end)
mock.patch(e, 'getMousePos', mock.func(2), function()
e:onInput{_MOUSE_L=true}
expect.eq(3, e.cursor)
end)
end

@ -1,18 +1,37 @@
local widgets = require('gui.widgets')
function test.hotkeylabel_click()
local func = mock.func()
local l = widgets.HotkeyLabel{key='SELECT', on_activate=func}
mock.patch(l, 'getMousePos', mock.func(0), function()
l:onInput{_MOUSE_L=true}
expect.eq(1, func.call_count)
end)
end
function test.togglehotkeylabel()
local toggle = widgets.ToggleHotkeyLabel{}
expect.true_(toggle:getOptionValue())
toggle:cycle()
expect.false_(toggle:getOptionValue())
toggle:cycle()
expect.true_(toggle:getOptionValue())
local toggle = widgets.ToggleHotkeyLabel{}
expect.true_(toggle:getOptionValue())
toggle:cycle()
expect.false_(toggle:getOptionValue())
toggle:cycle()
expect.true_(toggle:getOptionValue())
end
function test.togglehotkeylabel_default_value()
local toggle = widgets.ToggleHotkeyLabel{initial_option=2}
expect.false_(toggle:getOptionValue())
local toggle = widgets.ToggleHotkeyLabel{initial_option=2}
expect.false_(toggle:getOptionValue())
toggle = widgets.ToggleHotkeyLabel{initial_option=false}
expect.false_(toggle:getOptionValue())
end
toggle = widgets.ToggleHotkeyLabel{initial_option=false}
expect.false_(toggle:getOptionValue())
function test.togglehotkeylabel_click()
local l = widgets.ToggleHotkeyLabel{}
expect.true_(l:getOptionValue())
mock.patch(l, 'getMousePos', mock.func(0), function()
l:onInput{_MOUSE_L=true}
expect.false_(l:getOptionValue())
end)
end

@ -0,0 +1,648 @@
local h = require('helpdb')
local mock_plugin_db = {
hascommands={
boxbinders={description="Box your binders.", help=[[Box your binders.
This command will help you box your binders.]]},
bindboxers={description="Bind your boxers.", help=[[Bind your boxers.
This command will help you bind your boxers.]]}
},
samename={
samename={description="Samename.", help=[[Samename.
This command has the same name as its host plugin.]]}
},
nocommand={},
nodocs_hascommands={
nodoc_command={description="cpp description.", help=[[cpp description.
Rest of help.]]}
},
nodocs_samename={
nodocs_samename={description="Nodocs samename.", help=[[Nodocs samename.
This command has the same name as its host plugin but no rst docs.]]}
},
nodocs_nocommand={},
}
local mock_command_db = {}
for k,v in pairs(mock_plugin_db) do
for c,d in pairs(v) do
mock_command_db[c] = d
end
end
local mock_script_db = {
basic=true,
['subdir/scriptname']=true,
inscript_docs=true,
inscript_short_only=true,
nodocs_script=true,
}
local files = {
['hack/docs/docs/Tags.txt']=[[
* fort: Tools that are useful while in fort mode.
* armok: Tools that give you complete control over
an aspect of the game or provide access to
information that the game intentionally keeps
hidden.
* map: Tools that interact with the game map.
* units: Tools that interact with units.
* nomembers: Nothing is tagged with this.
]],
['hack/docs/docs/tools/hascommands.txt']=[[
hascommands
***********
**Tags:** fort | armok | units
Documented a plugin that
has commands.
**Command:** "boxbinders"
Documented boxbinders.
**Command:** "bindboxers"
Documented bindboxers.
Documented full help.
]],
['hack/docs/docs/tools/samename.txt']=[[
samename
********
**Tags:** fort | armok
| units
**Command:** "samename"
Documented samename.
Documented full help.
]],
['hack/docs/docs/tools/nocommand.txt']=[[
nocommand
*********
**Tags:** fort | armok |
units
Documented nocommand.
Documented full help.
]],
['hack/docs/docs/tools/basic.txt']=[[
basic
*****
**Tags:** map
**Command:** "basic"
Documented basic.
Documented full help.
]],
['hack/docs/docs/tools/subdir/scriptname.txt']=[[
subdir/scriptname
*****************
**Tags:** map
**Command:** "subdir/scriptname"
Documented subdir/scriptname.
Documented full help.
]],
['scripts/scriptpath/basic.lua']=[[
-- in-file short description for basic
-- [====[
basic
=====
**Tags:** map
**Command:** "basic"
in-file basic.
Documented full help.
]====]
script contents
]],
['scripts/scriptpath/subdir/scriptname.lua']=[[
-- in-file short description for scriptname
-- [====[
subdir/scriptname
=================
**Tags:** map
**Command:** "subdir/scriptname"
in-file scriptname.
Documented full help.
]====]
script contents
]],
['scripts/scriptpath/inscript_docs.lua']=[[
-- in-file short description for inscript_docs
-- [====[
inscript_docs
=============
**Tags:** map | badtag
**Command:** "inscript_docs"
in-file inscript_docs.
Documented full help.
]====]
script contents
]],
['scripts/scriptpath/nodocs_script.lua']=[[
script contents
]],
['scripts/scriptpath/inscript_short_only.lua']=[[
-- inscript short desc.
script contents
]],
['other/scriptpath/basic.lua']=[[
-- in-file short description for basic (other)
-- [====[
basic
=====
**Tags:** map
**Command:** "basic"
in-file basic (other).
Documented full help.
]====]
script contents
]],
['other/scriptpath/subdir/scriptname.lua']=[[
-- in-file short description for scriptname (other)
-- [====[
subdir/scriptname
=================
**Tags:** map
**Command:** "subdir/scriptname"
in-file scriptname (other).
Documented full help.
]====]
script contents
]],
['other/scriptpath/inscript_docs.lua']=[[
-- in-file short description for inscript_docs (other)
-- [====[
inscript_docs
=============
**Tags:** map
**Command:** "inscript_docs"
in-file inscript_docs (other).
Documented full help.
]====]
script contents
]],
}
local function mock_getCommandHelp(command)
if mock_command_db[command] then
return mock_command_db[command].help
end
end
local function mock_listPlugins()
local list = {}
for k in pairs(mock_plugin_db) do
table.insert(list, k)
end
return list
end
local function mock_listCommands(plugin)
local list = {}
for k,v in pairs(mock_plugin_db) do
if k == plugin then
for c in pairs(v) do
table.insert(list, c)
end
break
end
end
return list
end
local function mock_getCommandDescription(command)
if mock_command_db[command] then
return mock_command_db[command].description
end
end
local function mock_getScriptPaths()
return {'scripts/scriptpath', 'other/scriptpath'}
end
local function mock_mtime(path)
if files[path] then return 1 end
return -1
end
local function mock_listdir_recursive(script_path)
local list = {}
for s in pairs(mock_script_db) do
table.insert(list, {isdir=false, path=s..'.lua'})
end
return list
end
local function mock_getTickCount()
return 100000
end
local function mock_pcall(fn, fname)
if fn ~= io.lines then error('unexpected fn for pcall') end
if not files[fname] then
return false
end
return true, files[fname]:gmatch('([^\n]*)\n?')
end
config.wrapper = function(test_fn)
mock.patch({
{h.dfhack.internal, 'getCommandHelp', mock_getCommandHelp},
{h.dfhack.internal, 'listPlugins', mock_listPlugins},
{h.dfhack.internal, 'listCommands', mock_listCommands},
{h.dfhack.internal, 'getCommandDescription', mock_getCommandDescription},
{h.dfhack.internal, 'getScriptPaths', mock_getScriptPaths},
{h.dfhack.filesystem, 'mtime', mock_mtime},
{h.dfhack.filesystem, 'listdir_recursive', mock_listdir_recursive},
{h.dfhack, 'getTickCount', mock_getTickCount},
{h, 'pcall', mock_pcall},
}, test_fn)
end
function test.is_entry()
expect.true_(h.is_entry('ls'),
'builtin commands get an entry')
expect.true_(h.is_entry('hascommands'),
'plugins whose names do not match their commands get an entry')
expect.true_(h.is_entry('boxbinders'),
'commands whose name does not match the host plugin get an entry')
expect.true_(h.is_entry('samename'),
'plugins that have a command with the same name get one entry')
expect.true_(h.is_entry('nocommand'),
'plugins that do not have commands get an entry')
expect.true_(h.is_entry('basic'),
'scripts in the script path get an entry')
expect.true_(h.is_entry('subdir/scriptname'),
'scripts in subdirs of a script path get an entry')
expect.true_(h.is_entry('nodocs_hascommands'),
'plugins whose names do not match their commands get an entry')
expect.true_(h.is_entry('nodoc_command'),
'commands whose name does not match the host plugin get an entry')
expect.true_(h.is_entry('nodocs_samename'),
'plugins that have a command with the same name get one entry')
expect.true_(h.is_entry('nodocs_nocommand'),
'plugins that do not have commands get an entry')
expect.true_(h.is_entry('inscript_docs'),
'scripts in the script path get an entry')
expect.true_(h.is_entry('nodocs_script'),
'scripts in the script path get an entry')
expect.true_(h.is_entry('inscript_short_only'),
'scripts in the script path get an entry')
expect.false_(h.is_entry(nil),
'nil is not an entry')
expect.false_(h.is_entry(''),
'blank is not an entry')
expect.false_(h.is_entry('notanentryname'),
'strings that are neither plugin names nor command names do not get an entry')
expect.true_(h.is_entry({'hascommands', 'boxbinders', 'nocommand'}),
'list of valid entries')
expect.false_(h.is_entry({'hascommands', 'notanentryname'}),
'list contains an element that is not an entry')
end
function test.get_entry_types()
expect.table_eq({builtin=true, command=true}, h.get_entry_types('ls'))
expect.table_eq({plugin=true}, h.get_entry_types('hascommands'))
expect.table_eq({command=true}, h.get_entry_types('boxbinders'))
expect.table_eq({plugin=true, command=true}, h.get_entry_types('samename'))
expect.table_eq({plugin=true}, h.get_entry_types('nocommand'))
expect.table_eq({command=true}, h.get_entry_types('basic'))
expect.table_eq({command=true}, h.get_entry_types('subdir/scriptname'))
expect.table_eq({plugin=true}, h.get_entry_types('nodocs_hascommands'))
expect.table_eq({command=true}, h.get_entry_types('nodoc_command'))
expect.table_eq({plugin=true, command=true}, h.get_entry_types('nodocs_samename'))
expect.table_eq({plugin=true}, h.get_entry_types('nodocs_nocommand'))
expect.table_eq({command=true}, h.get_entry_types('nodocs_script'))
expect.table_eq({command=true}, h.get_entry_types('inscript_docs'))
expect.table_eq({command=true}, h.get_entry_types('inscript_short_only'))
expect.error_match('entry not found', function()
h.get_entry_types('notanentry') end)
end
function test.get_entry_short_help()
expect.eq('No help available.', h.get_entry_short_help('ls'),
'no docs for builtin fn result in default short description')
expect.eq('Documented a plugin that has commands.', h.get_entry_short_help('hascommands'))
expect.eq('Box your binders.', h.get_entry_short_help('boxbinders'),
'should get short help from command description')
expect.eq('Samename.', h.get_entry_short_help('samename'),
'should get short help from command description')
expect.eq('Documented nocommand.', h.get_entry_short_help('nocommand'))
expect.eq('Documented basic.', h.get_entry_short_help('basic'))
expect.eq('Documented subdir/scriptname.', h.get_entry_short_help('subdir/scriptname'))
expect.eq('No help available.', h.get_entry_short_help('nodocs_hascommands'))
expect.eq('cpp description.', h.get_entry_short_help('nodoc_command'),
'should get short help from command description')
expect.eq('Nodocs samename.', h.get_entry_short_help('nodocs_samename'),
'should get short help from command description')
expect.eq('No help available.', h.get_entry_short_help('nodocs_nocommand'))
expect.eq('in-file short description for inscript_docs.',
h.get_entry_short_help('inscript_docs'),
'should get short help from header comment')
expect.eq('No help available.',
h.get_entry_short_help('nodocs_script'))
expect.eq('inscript short desc.',
h.get_entry_short_help('inscript_short_only'),
'should get short help from header comment')
end
function test.get_entry_long_help()
-- long help for plugins/commands that have doc files should match the
-- contents of those files exactly
expect.eq(files['hack/docs/docs/tools/hascommands.txt'],
h.get_entry_long_help('hascommands'))
expect.eq(files['hack/docs/docs/tools/hascommands.txt'],
h.get_entry_long_help('boxbinders'))
expect.eq(files['hack/docs/docs/tools/samename.txt'],
h.get_entry_long_help('samename'))
expect.eq(files['hack/docs/docs/tools/nocommand.txt'],
h.get_entry_long_help('nocommand'))
expect.eq(files['hack/docs/docs/tools/basic.txt'],
h.get_entry_long_help('basic'))
expect.eq(files['hack/docs/docs/tools/subdir/scriptname.txt'],
h.get_entry_long_help('subdir/scriptname'))
-- plugins/commands that have no doc files get the default template
expect.eq([[ls
**
No help available.
]], h.get_entry_long_help('ls'))
expect.eq([[nodocs_hascommands
******************
No help available.
]], h.get_entry_long_help('nodocs_hascommands'))
expect.eq([[nodocs_hascommands
******************
No help available.
]], h.get_entry_long_help('nodoc_command'))
expect.eq([[Nodocs samename.
This command has the same name as its host plugin but no rst docs.]], h.get_entry_long_help('nodocs_samename'))
expect.eq([[nodocs_nocommand
****************
No help available.
]], h.get_entry_long_help('nodocs_nocommand'))
expect.eq([[nodocs_script
*************
No help available.
]], h.get_entry_long_help('nodocs_script'))
expect.eq([[inscript_short_only
*******************
No help available.
]], h.get_entry_long_help('inscript_short_only'))
-- scripts that have no doc files get the docs from the script lua source
expect.eq([[inscript_docs
=============
**Tags:** map | badtag
**Command:** "inscript_docs"
in-file inscript_docs.
Documented full help.]], h.get_entry_long_help('inscript_docs'))
end
function test.get_entry_tags()
expect.table_eq({fort=true, armok=true, units=true},
h.get_entry_tags('hascommands'))
expect.table_eq({fort=true, armok=true, units=true},
h.get_entry_tags('samename'))
expect.table_eq({fort=true, armok=true, units=true},
h.get_entry_tags('nocommand'))
expect.table_eq({map=true}, h.get_entry_tags('basic'))
expect.table_eq({map=true}, h.get_entry_tags('inscript_docs'),
'bad tags should get filtered out')
end
function test.is_tag()
-- see tags defined in the Tags.txt files entry above
expect.true_(h.is_tag('map'))
expect.true_(h.is_tag({'map', 'armok'}))
expect.false_(h.is_tag(nil))
expect.false_(h.is_tag(''))
expect.false_(h.is_tag('not_tag'))
expect.false_(h.is_tag({'map', 'not_tag', 'armok'}))
end
function test.get_tags()
expect.table_eq({'armok', 'fort', 'map', 'nomembers', 'units'},
h.get_tags())
end
function test.get_tag_data()
local tag_data = h.get_tag_data('armok')
table.sort(tag_data)
expect.table_eq({description='Tools that give you complete control over an aspect of the game or provide access to information that the game intentionally keeps hidden.',
'bindboxers', 'boxbinders', 'hascommands', 'nocommand', 'samename'},
tag_data,
'multi-line descriptions should get joined into a single line.')
tag_data = h.get_tag_data('fort')
table.sort(tag_data)
expect.table_eq({description='Tools that are useful while in fort mode.',
'bindboxers', 'boxbinders', 'hascommands', 'nocommand', 'samename'},
tag_data)
tag_data = h.get_tag_data('units')
table.sort(tag_data)
expect.table_eq({description='Tools that interact with units.',
'bindboxers', 'boxbinders', 'hascommands', 'nocommand', 'samename'},
tag_data)
tag_data = h.get_tag_data('map')
table.sort(tag_data)
expect.table_eq({description='Tools that interact with the game map.',
'basic', 'inscript_docs', 'subdir/scriptname'},
tag_data)
expect.table_eq({description='Nothing is tagged with this.'}, h.get_tag_data('nomembers'))
expect.error_match('tag not found',
function() h.get_tag_data('notatag') end)
end
function test.search_entries()
-- all entries, in alphabetical order by last path component
local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders',
'clear', 'cls', 'die', 'dir', 'disable', 'devel/dump-rpc', 'enable',
'fpause', 'hascommands', 'help', 'hide', 'inscript_docs',
'inscript_short_only', 'keybinding', 'kill-lua', 'load', 'ls', 'man',
'nocommand', 'nodoc_command', 'nodocs_hascommands', 'nodocs_nocommand',
'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename',
'script', 'subdir/scriptname', 'sc-script', 'show', 'tags', 'type',
'unload'}
table.sort(expected, h.sort_by_basename)
expect.table_eq(expected, h.search_entries())
expect.table_eq(expected, h.search_entries({}))
expect.table_eq(expected, h.search_entries(nil, {}))
expect.table_eq(expected, h.search_entries({}, {}))
expected = {'inscript_docs', 'subdir/scriptname'}
expect.table_eq(expected, h.search_entries({tag='map', str='script'}))
expected = {'script', 'sc-script'}
table.sort(expected, h.sort_by_basename)
expect.table_eq(expected, h.search_entries({str='script',
entry_type='builtin'}))
expected = {'inscript_docs', 'inscript_short_only','nodocs_script',
'subdir/scriptname'}
expect.table_eq(expected, h.search_entries({str='script'},
{entry_type='builtin'}))
expected = {'bindboxers', 'boxbinders'}
expect.table_eq(expected, h.search_entries({str='box'}))
end
function test.get_commands()
local expected = {'?', 'alias', 'basic', 'bindboxers', 'boxbinders',
'clear', 'cls', 'die', 'dir', 'disable', 'devel/dump-rpc', 'enable',
'fpause', 'help', 'hide', 'inscript_docs', 'inscript_short_only',
'keybinding', 'kill-lua', 'load', 'ls', 'man', 'nodoc_command',
'nodocs_samename', 'nodocs_script', 'plug', 'reload', 'samename',
'script', 'subdir/scriptname', 'sc-script', 'show', 'tags', 'type',
'unload'}
table.sort(expected, h.sort_by_basename)
expect.table_eq(expected, h.get_commands())
end
function test.is_builtin()
expect.true_(h.is_builtin('ls'))
expect.false_(h.is_builtin('basic'))
expect.false_(h.is_builtin('notanentry'))
end
function test.help()
expect.printerr_match('No help entry found', function() h.help('blarg') end)
local mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.help('nocommand')
expect.eq(1, mock_print.call_count)
expect.eq(files['hack/docs/docs/tools/nocommand.txt'],
mock_print.call_args[1][1])
end)
end
function test.tags()
local mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.tags()
expect.eq(7, mock_print.call_count)
expect.eq('armok Tools that give you complete control over an aspect of the',
mock_print.call_args[1][1])
expect.eq(' game or provide access to information that the game',
mock_print.call_args[2][1])
expect.eq(' intentionally keeps hidden.',
mock_print.call_args[3][1])
expect.eq('fort Tools that are useful while in fort mode.',
mock_print.call_args[4][1])
expect.eq('map Tools that interact with the game map.',
mock_print.call_args[5][1])
expect.eq('nomembers Nothing is tagged with this.',
mock_print.call_args[6][1])
expect.eq('units Tools that interact with units.',
mock_print.call_args[7][1])
end)
end
function test.ls()
local mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.ls('doc') -- interpreted as a string
expect.eq(5, mock_print.call_count)
expect.eq('inscript_docs in-file short description for inscript_docs.',
mock_print.call_args[1][1])
expect.eq(' tags: map', mock_print.call_args[2][1])
expect.eq('nodoc_command cpp description.',
mock_print.call_args[3][1])
expect.eq('nodocs_samename Nodocs samename.',
mock_print.call_args[4][1])
expect.eq('nodocs_script No help available.',
mock_print.call_args[5][1])
end)
mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.ls('armok') -- interpreted as a tag
expect.eq(6, mock_print.call_count)
expect.eq('bindboxers Bind your boxers.',
mock_print.call_args[1][1])
expect.eq(' tags: armok, fort, units',
mock_print.call_args[2][1])
expect.eq('boxbinders Box your binders.',
mock_print.call_args[3][1])
expect.eq(' tags: armok, fort, units',
mock_print.call_args[4][1])
expect.eq('samename Samename.',
mock_print.call_args[5][1])
expect.eq(' tags: armok, fort, units',
mock_print.call_args[6][1])
end)
mock_print = mock.func()
mock.patch(h, 'print', mock_print, function()
h.ls('not a match')
expect.eq(1, mock_print.call_count)
expect.eq('No matches.', mock_print.call_args[1][1])
end)
end