diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index bef01888f..e922ecaca 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 758bf225f..c87154594 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 55a217757..d1dbc2856 100644 --- a/docs/Lua API.rst +++ b/docs/Lua API.rst @@ -3029,6 +3029,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 ====== 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/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 338db4ab9..969d3774a 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -188,7 +188,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/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