diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fc6eac094..83f3490da 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 \ diff --git a/docs/Dev-intro.rst b/docs/Dev-intro.rst index 09a68a0f9..a49cb96f6 100644 --- a/docs/Dev-intro.rst +++ b/docs/Dev-intro.rst @@ -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 diff --git a/docs/Lua API.rst b/docs/Lua API.rst index d436bdd5c..24b9a7509 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -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. diff --git a/docs/changelog.txt b/docs/changelog.txt index 2733cd539..2639de182 100644 --- a/docs/changelog.txt +++ b/docs/changelog.txt @@ -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 diff --git a/library/lua/argparse.lua b/library/lua/argparse.lua index ee170c190..e094bbb57 100644 --- a/library/lua/argparse.lua +++ b/library/lua/argparse.lua @@ -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) diff --git a/library/lua/gui/widgets.lua b/library/lua/gui/widgets.lua index 3ac74bb52..d1801e764 100644 --- a/library/lua/gui/widgets.lua +++ b/library/lua/gui/widgets.lua @@ -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() diff --git a/library/lua/helpdb.lua b/library/lua/helpdb.lua index a6c635ee0..e6bd9133b 100644 --- a/library/lua/helpdb.lua +++ b/library/lua/helpdb.lua @@ -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 diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index dae5d76e0..49e704b75 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -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) diff --git a/plugins/examples/persistent_per_save_example.cpp b/plugins/examples/persistent_per_save_example.cpp new file mode 100644 index 000000000..5a7bf5224 --- /dev/null +++ b/plugins/examples/persistent_per_save_example.cpp @@ -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 +#include + +#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 ¶meters); +static void do_cycle(color_ostream &out); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &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 ¶meters) { + // 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 +} diff --git a/plugins/examples/simple_command_example.cpp b/plugins/examples/simple_command_example.cpp new file mode 100644 index 000000000..7b12a1271 --- /dev/null +++ b/plugins/examples/simple_command_example.cpp @@ -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 +#include + +#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 ¶meters); + +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &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 ¶meters) { + // be sure to suspend the core if any DF state is read or modified + CoreSuspender suspend; + + // TODO: command logic + + return CR_OK; +} diff --git a/plugins/examples/skeleton.cpp b/plugins/examples/skeleton.cpp new file mode 100644 index 000000000..539d84b1a --- /dev/null +++ b/plugins/examples/skeleton.cpp @@ -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 +#include + +#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 ¶meters); + +// run when the plugin is loaded +DFhackCExport command_result plugin_init(color_ostream &out, std::vector &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 ¶meters) { + 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; +} diff --git a/plugins/examples/ui_addition_example.cpp b/plugins/examples/ui_addition_example.cpp new file mode 100644 index 000000000..bbd3af3de --- /dev/null +++ b/plugins/examples/ui_addition_example.cpp @@ -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 +#include + +#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; +} diff --git a/plugins/seedwatch.cpp b/plugins/seedwatch.cpp index 17a765bcf..13ed8e66e 100644 --- a/plugins/seedwatch.cpp +++ b/plugins/seedwatch.cpp @@ -91,7 +91,9 @@ command_result df_seedwatch(color_ostream &out, vector& parameters) map 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& 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 { diff --git a/plugins/skeleton/CMakeLists.txt b/plugins/skeleton/CMakeLists.txt deleted file mode 100644 index cbe5f7ce6..000000000 --- a/plugins/skeleton/CMakeLists.txt +++ /dev/null @@ -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}) diff --git a/plugins/skeleton/skeleton.cpp b/plugins/skeleton/skeleton.cpp deleted file mode 100644 index 7d5936f6d..000000000 --- a/plugins/skeleton/skeleton.cpp +++ /dev/null @@ -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 -#include -#include -#include -// 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 ¶meters); - -DFhackCExport command_result plugin_init(color_ostream &out, std::vector &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