Merge pull request #673 from lethosor/plugin-manager

PluginManager improvements
develop
Lethosor 2015-08-24 17:40:48 -04:00
commit 001f9e79b0
11 changed files with 508 additions and 231 deletions

@ -9,12 +9,14 @@ DFHack Future
Stopped DF window from receiving input when unfocused on OS X Stopped DF window from receiving input when unfocused on OS X
Fixed issues with keybindings involving Ctrl-A and Ctrl-Z, as well as Alt-E/U/N on OS X Fixed issues with keybindings involving Ctrl-A and Ctrl-Z, as well as Alt-E/U/N on OS X
Multiple contexts can now be specified when adding keybindings Multiple contexts can now be specified when adding keybindings
Plugin system is no longer restricted to plugins that exist on startup
Lua Lua
Scripts can be enabled with the built-in enable/disable commands Scripts can be enabled with the built-in enable/disable commands
A new function, reqscript(), is available as a safer alternative to script_environment() A new function, reqscript(), is available as a safer alternative to script_environment()
Lua viewscreens can choose not to intercept the OPTIONS keybinding Lua viewscreens can choose not to intercept the OPTIONS keybinding
New internal commands New internal commands
kill-lua: Interrupt running Lua scripts kill-lua: Interrupt running Lua scripts
type: Show where a command is implemented
New plugins New plugins
confirm: Adds confirmation dialogs for several potentially dangerous actions confirm: Adds confirmation dialogs for several potentially dangerous actions
fix-unit-occupancy: Fixes issues with unit occupancy, such as faulty "unit blocking tile" messages (bug 3499) fix-unit-occupancy: Fixes issues with unit occupancy, such as faulty "unit blocking tile" messages (bug 3499)
@ -40,6 +42,7 @@ DFHack Future
- fixed assigning quality to items - fixed assigning quality to items
- made "esc" work properly - made "esc" work properly
gui/gm-editor handles lua tables properly gui/gm-editor handles lua tables properly
help: now recognizes built-in commands, like "help"
manipulator: fixed crash when selecting custom professions when none are found manipulator: fixed crash when selecting custom professions when none are found
remotefortressreader: fixed crash when attempting to send map info when no map was loaded remotefortressreader: fixed crash when attempting to send map info when no map was loaded
search: fixed crash in unit list after cancelling a job search: fixed crash in unit list after cancelling a job
@ -74,6 +77,9 @@ DFHack Future
- "lever pull" can be used to pull the currently-selected lever - "lever pull" can be used to pull the currently-selected lever
memview: Fixed display issue memview: Fixed display issue
nyan: Can now be stopped with dfhack-run nyan: Can now be stopped with dfhack-run
plug:
- lists all plugins
- shows state and number of commands in plugins
quicksave: Restricted to fortress mode quicksave: Restricted to fortress mode
remotefortressreader: Exposes more information remotefortressreader: Exposes more information
search: search:

@ -386,12 +386,12 @@ static bool try_autocomplete(color_ostream &con, const std::string &first, std::
std::vector<std::string> possible; std::vector<std::string> possible;
auto plug_mgr = Core::getInstance().getPluginManager(); auto plug_mgr = Core::getInstance().getPluginManager();
for(size_t i = 0; i < plug_mgr->size(); i++) for (auto it = plug_mgr->begin(); it != plug_mgr->end(); ++it)
{ {
const Plugin * plug = (plug_mgr->operator[](i)); const Plugin * plug = it->second;
for (size_t j = 0; j < plug->size(); j++) for (size_t j = 0; j < plug->size(); j++)
{ {
const PluginCommand &pcmd = plug->operator[](j); const PluginCommand &pcmd = (*plug)[j];
if (pcmd.isHotkeyCommand()) if (pcmd.isHotkeyCommand())
continue; continue;
if (pcmd.name.substr(0, first.size()) == first) if (pcmd.name.substr(0, first.size()) == first)
@ -485,6 +485,43 @@ static std::string sc_event_name (state_change_event id) {
return "SC_UNKNOWN"; return "SC_UNKNOWN";
} }
string getBuiltinCommand(std::string cmd)
{
std::string builtin = "";
if (cmd == "ls" ||
cmd == "help" ||
cmd == "type" ||
cmd == "load" ||
cmd == "unload" ||
cmd == "reload" ||
cmd == "enable" ||
cmd == "disable" ||
cmd == "plug" ||
cmd == "type" ||
cmd == "keybinding" ||
cmd == "fpause" ||
cmd == "cls" ||
cmd == "die" ||
cmd == "kill-lua" ||
cmd == "script" ||
cmd == "hide" ||
cmd == "show" ||
cmd == "sc-script"
)
builtin = cmd;
else if (cmd == "?" || cmd == "man")
builtin = "help";
else if (cmd == "dir")
builtin = "ls";
else if (cmd == "clear")
builtin = "cls";
return builtin;
}
command_result Core::runCommand(color_ostream &con, const std::string &first_, vector<string> &parts) command_result Core::runCommand(color_ostream &con, const std::string &first_, vector<string> &parts)
{ {
std::string first = first_; std::string first = first_;
@ -499,8 +536,10 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
first[i] = '/'; first[i] = '/';
} }
} }
// let's see what we actually got // let's see what we actually got
if(first=="help" || first == "?" || first == "man") string builtin = getBuiltinCommand(first);
if (builtin == "help")
{ {
if(!parts.size()) if(!parts.size())
{ {
@ -516,21 +555,26 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
" help|?|man - This text.\n" " help|?|man - This text.\n"
" help COMMAND - Usage help for the given command.\n" " help COMMAND - Usage help for the given command.\n"
" ls|dir [-a] [PLUGIN] - List available commands. Optionally for single plugin.\n" " ls|dir [-a] [PLUGIN] - List available commands. Optionally for single plugin.\n"
" cls - Clear the console.\n" " cls|clear - Clear the console.\n"
" fpause - Force DF to pause.\n" " fpause - Force DF to pause.\n"
" die - Force DF to close immediately\n" " die - Force DF to close immediately\n"
" keybinding - Modify bindings of commands to keys\n" " keybinding - Modify bindings of commands to keys\n"
"Plugin management (useful for developers):\n" "Plugin management (useful for developers):\n"
" plug [PLUGIN|v] - List plugin state and description.\n" " plug [PLUGIN|v] - List plugin state and description.\n"
" load PLUGIN|all - Load a plugin by name or load all possible plugins.\n" " load PLUGIN|-all - Load a plugin by name or load all possible plugins.\n"
" unload PLUGIN|all - Unload a plugin or all loaded plugins.\n" " unload PLUGIN|-all - Unload a plugin or all loaded plugins.\n"
" reload PLUGIN|all - Reload a plugin or all loaded plugins.\n" " reload PLUGIN|-all - Reload a plugin or all loaded plugins.\n"
); );
con.print("\nDFHack version %s.\n", Version::dfhack_version()); con.print("\nDFHack version %s.\n", Version::dfhack_version());
} }
else if (parts.size() == 1) else if (parts.size() == 1)
{ {
if (getBuiltinCommand(parts[0]).size())
{
con << parts[0] << ": built-in command; Use `ls`, `help`, or check hack/Readme.html for more information" << std::endl;
return CR_NOT_IMPLEMENTED;
}
Plugin *plug = plug_mgr->getPluginByCommand(parts[0]); Plugin *plug = plug_mgr->getPluginByCommand(parts[0]);
if (plug) { if (plug) {
for (size_t j = 0; j < plug->size();j++) for (size_t j = 0; j < plug->size();j++)
@ -564,97 +608,58 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
} }
} }
con.printerr("Unknown command: %s\n", parts[0].c_str()); con.printerr("Unknown command: %s\n", parts[0].c_str());
return CR_FAILURE;
} }
else else
{ {
con.printerr("not implemented yet\n"); con.printerr("not implemented yet\n");
return CR_NOT_IMPLEMENTED;
} }
} }
else if( first == "load" ) else if (builtin == "load" || builtin == "unload" || builtin == "reload")
{ {
if(parts.size()) bool all = false;
{ bool load = (builtin == "load");
string & plugname = parts[0]; bool unload = (builtin == "unload");
if(plugname == "all") if (parts.size())
{
for(size_t i = 0; i < plug_mgr->size();i++)
{
Plugin * plug = (plug_mgr->operator[](i));
plug->load(con);
}
}
else
{
Plugin * plug = plug_mgr->getPluginByName(plugname);
if(!plug)
{
con.printerr("No such plugin\n");
}
else
{
plug->load(con);
}
}
}
}
else if( first == "reload" )
{
if(parts.size())
{ {
string & plugname = parts[0]; for (auto p = parts.begin(); p != parts.end(); p++)
if(plugname == "all")
{ {
for(size_t i = 0; i < plug_mgr->size();i++) if (p->size() && (*p)[0] == '-')
{ {
Plugin * plug = (plug_mgr->operator[](i)); if (p->find('a') != string::npos)
plug->reload(con); all = true;
} }
} }
else if (all)
{ {
Plugin * plug = plug_mgr->getPluginByName(plugname); if (load)
if(!plug) plug_mgr->loadAll();
{ else if (unload)
con.printerr("No such plugin\n"); plug_mgr->unloadAll();
}
else else
{ plug_mgr->reloadAll();
plug->reload(con); return CR_OK;
}
}
}
}
else if( first == "unload" )
{
if(parts.size())
{
string & plugname = parts[0];
if(plugname == "all")
{
for(size_t i = 0; i < plug_mgr->size();i++)
{
Plugin * plug = (plug_mgr->operator[](i));
plug->unload(con);
}
} }
else for (auto p = parts.begin(); p != parts.end(); p++)
{ {
Plugin * plug = plug_mgr->getPluginByName(plugname); if (!p->size() || (*p)[0] == '-')
if(!plug) continue;
{ if (load)
con.printerr("No such plugin\n"); plug_mgr->load(*p);
} else if (unload)
plug_mgr->unload(*p);
else else
{ plug_mgr->reload(*p);
plug->unload(con);
}
} }
} }
else
con.printerr("%s: no arguments\n", builtin.c_str());
} }
else if( first == "enable" || first == "disable" ) else if( builtin == "enable" || builtin == "disable" )
{ {
CoreSuspender suspend; CoreSuspender suspend;
bool enable = (first == "enable"); bool enable = (builtin == "enable");
if(parts.size()) if(parts.size())
{ {
@ -673,7 +678,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
} }
} }
Plugin * plug = plug_mgr->getPluginByName(part); Plugin * plug = (*plug_mgr)[part];
if(!plug) if(!plug)
{ {
@ -691,14 +696,14 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
else if (!plug->can_set_enabled()) else if (!plug->can_set_enabled())
{ {
res = CR_NOT_IMPLEMENTED; res = CR_NOT_IMPLEMENTED;
con.printerr("Cannot %s plugin: %s\n", first.c_str(), part.c_str()); con.printerr("Cannot %s plugin: %s\n", builtin.c_str(), part.c_str());
} }
else else
{ {
res = plug->set_enabled(con, enable); res = plug->set_enabled(con, enable);
if (res != CR_OK || plug->is_enabled() != enable) if (res != CR_OK || plug->is_enabled() != enable)
con.printerr("Could not %s plugin: %s\n", first.c_str(), part.c_str()); con.printerr("Could not %s plugin: %s\n", builtin.c_str(), part.c_str());
} }
} }
@ -706,9 +711,9 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
} }
else else
{ {
for(size_t i = 0; i < plug_mgr->size();i++) for (auto it = plug_mgr->begin(); it != plug_mgr->end(); ++it)
{ {
Plugin * plug = (plug_mgr->operator[](i)); Plugin * plug = it->second;
if (!plug->can_be_enabled()) continue; if (!plug->can_be_enabled()) continue;
con.print( con.print(
@ -720,7 +725,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
} }
} }
} }
else if(first == "ls" || first == "dir") else if (builtin == "ls" || builtin == "dir")
{ {
bool all = false; bool all = false;
if (parts.size() && parts[0] == "-a") if (parts.size() && parts[0] == "-a")
@ -731,10 +736,18 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
if(parts.size()) if(parts.size())
{ {
string & plugname = parts[0]; string & plugname = parts[0];
const Plugin * plug = plug_mgr->getPluginByName(plugname); const Plugin * plug = (*plug_mgr)[plugname];
if(!plug) if(!plug)
{ {
con.printerr("There's no plugin called %s!\n",plugname.c_str()); con.printerr("There's no plugin called %s!\n", plugname.c_str());
}
else if (plug->getState() != Plugin::PS_LOADED)
{
con.printerr("Plugin %s is not loaded.\n", plugname.c_str());
}
else if (!plug->size())
{
con.printerr("Plugin %s is loaded but does not implement any commands.\n", plugname.c_str());
} }
else for (size_t j = 0; j < plug->size();j++) else for (size_t j = 0; j < plug->size();j++)
{ {
@ -749,27 +762,28 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
{ {
con.print( con.print(
"builtin:\n" "builtin:\n"
" help|?|man - This text or help specific to a plugin.\n" " help|?|man - This text or help specific to a plugin.\n"
" ls [-a] [PLUGIN] - List available commands. Optionally for single plugin.\n" " ls|dir [-a] [PLUGIN] - List available commands. Optionally for single plugin.\n"
" cls - Clear the console.\n" " cls|clear - Clear the console.\n"
" fpause - Force DF to pause.\n" " fpause - Force DF to pause.\n"
" die - Force DF to close immediately\n" " die - Force DF to close immediately\n"
" kill-lua - Stop an active Lua script\n" " kill-lua - Stop an active Lua script\n"
" keybinding - Modify bindings of commands to keys\n" " keybinding - Modify bindings of commands to keys\n"
" script FILENAME - Run the commands specified in a file.\n" " script FILENAME - Run the commands specified in a file.\n"
" sc-script - Automatically run specified scripts on state change events\n" " sc-script - Automatically run specified scripts on state change events\n"
" plug [PLUGIN|v] - List plugin state and detailed description.\n" " plug [PLUGIN|v] - List plugin state and detailed description.\n"
" load PLUGIN|all - Load a plugin by name or load all possible plugins.\n" " load PLUGIN|-all [...] - Load a plugin by name or load all possible plugins.\n"
" unload PLUGIN|all - Unload a plugin or all loaded plugins.\n" " unload PLUGIN|-all [...] - Unload a plugin or all loaded plugins.\n"
" reload PLUGIN|all - Reload a plugin or all loaded plugins.\n" " reload PLUGIN|-all [...] - Reload a plugin or all loaded plugins.\n"
" enable/disable PLUGIN - Enable or disable a plugin if supported.\n" " enable/disable PLUGIN [...] - Enable or disable a plugin if supported.\n"
" type COMMAND - Display information about where a command is implemented\n"
"\n" "\n"
"plugins:\n" "plugins:\n"
); );
std::set <sortable> out; std::set <sortable> out;
for(size_t i = 0; i < plug_mgr->size();i++) for (auto it = plug_mgr->begin(); it != plug_mgr->end(); ++it)
{ {
const Plugin * plug = (plug_mgr->operator[](i)); const Plugin * plug = it->second;
if(!plug->size()) if(!plug->size())
continue; continue;
for (size_t j = 0; j < plug->size();j++) for (size_t j = 0; j < plug->size();j++)
@ -795,17 +809,85 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
} }
} }
} }
else if(first == "plug") else if (builtin == "plug")
{ {
for(size_t i = 0; i < plug_mgr->size();i++) const char *header_format = "%25s %10s %4s\n";
const char *row_format = "%25s %10s %4i\n";
con.print(header_format, "Name", "State", "Cmds");
plug_mgr->refresh();
for (auto it = plug_mgr->begin(); it != plug_mgr->end(); ++it)
{ {
const Plugin * plug = (plug_mgr->operator[](i)); const Plugin * plug = it->second;
if(!plug->size()) if (!plug)
continue; continue;
con.print("%s\n", plug->getName().c_str()); if (parts.size() && std::find(parts.begin(), parts.end(), plug->getName()) == parts.end())
continue;
color_value color;
switch (plug->getState())
{
case Plugin::PS_LOADED:
color = COLOR_LIGHTGREEN;
break;
case Plugin::PS_UNLOADED:
case Plugin::PS_UNLOADING:
color = COLOR_YELLOW;
break;
case Plugin::PS_LOADING:
color = COLOR_LIGHTBLUE;
break;
case Plugin::PS_BROKEN:
color = COLOR_LIGHTRED;
break;
default:
color = COLOR_LIGHTMAGENTA;
break;
}
con.color(color);
con.print(row_format,
plug->getName().c_str(),
Plugin::getStateDescription(plug->getState()),
plug->size()
);
con.color(COLOR_RESET);
}
}
else if (builtin == "type")
{
if (!parts.size())
{
con.printerr("type: no argument\n");
return CR_WRONG_USAGE;
}
con << parts[0];
string builtin_cmd = getBuiltinCommand(parts[0]);
Plugin *plug = plug_mgr->getPluginByCommand(parts[0]);
if (builtin_cmd.size())
{
con << " is a built-in command";
if (builtin_cmd != parts[0])
con << " (aliased to " << builtin_cmd << ")";
con << "." << std::endl;
}
else if (plug)
{
con << " is part of plugin " << plug->getName() << "." << std::endl;
}
else if (findScript(this->p->getPath(), parts[0] + ".lua").size())
{
con << " is a Lua script." << std::endl;
}
else if (findScript(this->p->getPath(), parts[0] + ".rb").size())
{
con << " is a Ruby script." << std::endl;
}
else
{
con << " is not recognized." << std::endl;
return CR_FAILURE;
} }
} }
else if(first == "keybinding") else if (builtin == "keybinding")
{ {
if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add"))
{ {
@ -853,12 +935,12 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
<< Gui::getFocusString(Core::getTopViewscreen()) << endl; << Gui::getFocusString(Core::getTopViewscreen()) << endl;
} }
} }
else if(first == "fpause") else if (builtin == "fpause")
{ {
World::SetPauseState(true); World::SetPauseState(true);
con.print("The game was forced to pause!\n"); con.print("The game was forced to pause!\n");
} }
else if(first == "cls") else if (builtin == "cls")
{ {
if (con.is_console()) if (con.is_console())
((Console&)con).clear(); ((Console&)con).clear();
@ -868,11 +950,11 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
return CR_NEEDS_CONSOLE; return CR_NEEDS_CONSOLE;
} }
} }
else if(first == "die") else if (builtin == "die")
{ {
_exit(666); _exit(666);
} }
else if(first == "kill-lua") else if (builtin == "kill-lua")
{ {
bool force = false; bool force = false;
for (auto it = parts.begin(); it != parts.end(); ++it) for (auto it = parts.begin(); it != parts.end(); ++it)
@ -883,7 +965,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
if (!Lua::Interrupt(force)) if (!Lua::Interrupt(force))
con.printerr("Failed to register hook - use 'kill-lua force' to force\n"); con.printerr("Failed to register hook - use 'kill-lua force' to force\n");
} }
else if(first == "script") else if (builtin == "script")
{ {
if(parts.size() == 1) if(parts.size() == 1)
{ {
@ -896,7 +978,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
return CR_WRONG_USAGE; return CR_WRONG_USAGE;
} }
} }
else if(first=="hide") else if (builtin=="hide")
{ {
if (!getConsole().hide()) if (!getConsole().hide())
{ {
@ -905,7 +987,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
} }
return CR_OK; return CR_OK;
} }
else if(first=="show") else if (builtin=="show")
{ {
if (!getConsole().show()) if (!getConsole().show())
{ {
@ -914,7 +996,7 @@ command_result Core::runCommand(color_ostream &con, const std::string &first_, v
} }
return CR_OK; return CR_OK;
} }
else if(first == "sc-script") else if (builtin == "sc-script")
{ {
if (parts.size() < 1) if (parts.size() < 1)
{ {
@ -1338,7 +1420,7 @@ bool Core::Init()
cerr << "Initializing Plugins.\n"; cerr << "Initializing Plugins.\n";
// create plugin manager // create plugin manager
plug_mgr = new PluginManager(this); plug_mgr = new PluginManager(this);
plug_mgr->init(this); plug_mgr->init();
IODATA *temp = new IODATA; IODATA *temp = new IODATA;
temp->core = this; temp->core = this;
temp->plug_mgr = plug_mgr; temp->plug_mgr = plug_mgr;

@ -39,6 +39,7 @@ distribution.
#include "DataIdentity.h" #include "DataIdentity.h"
#include "DataFuncs.h" #include "DataFuncs.h"
#include "DFHackVersion.h" #include "DFHackVersion.h"
#include "PluginManager.h"
#include "modules/World.h" #include "modules/World.h"
#include "modules/Gui.h" #include "modules/Gui.h"

@ -49,6 +49,7 @@ distribution.
#include "MiscUtils.h" #include "MiscUtils.h"
#include "DFHackVersion.h" #include "DFHackVersion.h"
#include "PluginManager.h"
#include "df/job.h" #include "df/job.h"
#include "df/job_item.h" #include "df/job_item.h"

@ -40,6 +40,7 @@ distribution.
#include "LuaTools.h" #include "LuaTools.h"
#include "DataFuncs.h" #include "DataFuncs.h"
#include "PluginManager.h"
#include "MiscUtils.h" #include "MiscUtils.h"
#include <lua.h> #include <lua.h>

@ -52,6 +52,31 @@ using namespace tthread;
#include <assert.h> #include <assert.h>
#define MUTEX_GUARD(lock) auto lock_##__LINE__ = make_mutex_guard(lock);
template <typename T>
tthread::lock_guard<T> make_mutex_guard (T *mutex)
{
return tthread::lock_guard<T>(*mutex);
}
#if defined(_LINUX)
static const string plugin_suffix = ".plug.so";
#elif defined(_DARWIN)
static const string plugin_suffix = ".plug.dylib";
#else
static const string plugin_suffix = ".plug.dll";
#endif
static string getPluginPath()
{
return Core::getInstance().getHackPath() + "plugins/";
}
static string getPluginPath (std::string name)
{
return getPluginPath() + name + plugin_suffix;
}
struct Plugin::RefLock struct Plugin::RefLock
{ {
RefLock() RefLock()
@ -156,18 +181,12 @@ struct Plugin::LuaEvent : public Lua::Event::Owner {
} }
}; };
Plugin::Plugin(Core * core, const std::string & filepath, const std::string & _filename, PluginManager * pm) Plugin::Plugin(Core * core, const std::string & path,
const std::string &name, PluginManager * pm)
:name(name),
path(path),
parent(pm)
{ {
filename = filepath;
parent = pm;
name.reserve(_filename.size());
for(size_t i = 0; i < _filename.size();i++)
{
char ch = _filename[i];
if(ch == '.')
break;
name.append(1,ch);
}
plugin_lib = 0; plugin_lib = 0;
plugin_init = 0; plugin_init = 0;
plugin_globals = 0; plugin_globals = 0;
@ -191,6 +210,24 @@ Plugin::~Plugin()
delete access; delete access;
} }
const char *Plugin::getStateDescription (plugin_state state)
{
switch (state)
{
#define map(s, desc) case s: return desc; break;
map(PS_LOADED, "loaded")
map(PS_UNLOADED, "unloaded")
map(PS_BROKEN, "broken")
map(PS_LOADING, "loading")
map(PS_UNLOADING, "unloading")
map(PS_DELETED, "deleted")
#undef map
default:
return "unknown";
break;
}
}
bool Plugin::load(color_ostream &con) bool Plugin::load(color_ostream &con)
{ {
{ {
@ -199,8 +236,10 @@ bool Plugin::load(color_ostream &con)
{ {
return true; return true;
} }
else if(state != PS_UNLOADED) else if(state != PS_UNLOADED && state != PS_DELETED)
{ {
if (state == PS_BROKEN)
con.printerr("Plugin %s is broken - cannot be loaded\n", name.c_str());
return false; return false;
} }
state = PS_LOADING; state = PS_LOADING;
@ -208,16 +247,24 @@ bool Plugin::load(color_ostream &con)
// enter suspend // enter suspend
CoreSuspender suspend; CoreSuspender suspend;
// open the library, etc // open the library, etc
fprintf(stderr, "loading plugin %s\n", filename.c_str()); fprintf(stderr, "loading plugin %s\n", name.c_str());
DFLibrary * plug = OpenPlugin(filename.c_str()); DFLibrary * plug = OpenPlugin(path.c_str());
if(!plug) if(!plug)
{ {
con.printerr("Can't load plugin %s\n", filename.c_str());
RefAutolock lock(access); RefAutolock lock(access);
state = PS_BROKEN; if (!Filesystem::isfile(path))
return false; {
con.printerr("Plugin %s does not exist on disk\n", name.c_str());
state = PS_DELETED;
return false;
}
else {
con.printerr("Can't load plugin %s\n", name.c_str());
state = PS_UNLOADED;
return false;
}
} }
#define plugin_abort_load ClosePlugin(plug); RefAutolock lock(access); state = PS_BROKEN #define plugin_abort_load ClosePlugin(plug); RefAutolock lock(access); state = PS_UNLOADED
#define plugin_check_symbol(sym) \ #define plugin_check_symbol(sym) \
if (!LookupPlugin(plug, sym)) \ if (!LookupPlugin(plug, sym)) \
{ \ { \
@ -226,14 +273,20 @@ bool Plugin::load(color_ostream &con)
return false; \ return false; \
} }
plugin_check_symbol("name") plugin_check_symbol("plugin_name")
plugin_check_symbol("version") plugin_check_symbol("plugin_version")
plugin_check_symbol("plugin_self") plugin_check_symbol("plugin_self")
plugin_check_symbol("plugin_init") plugin_check_symbol("plugin_init")
plugin_check_symbol("plugin_globals") plugin_check_symbol("plugin_globals")
const char ** plug_name =(const char ** ) LookupPlugin(plug, "name"); const char ** plug_name =(const char ** ) LookupPlugin(plug, "plugin_name");
const char ** plug_version =(const char ** ) LookupPlugin(plug, "version"); if (name != *plug_name)
const char ** plug_git_desc_ptr = (const char**) LookupPlugin(plug, "git_description"); {
con.printerr("Plugin %s: name mismatch, claims to be %s\n", name.c_str(), *plug_name);
plugin_abort_load;
return false;
}
const char ** plug_version =(const char ** ) LookupPlugin(plug, "plugin_version");
const char ** plug_git_desc_ptr = (const char**) LookupPlugin(plug, "plugin_git_description");
Plugin **plug_self = (Plugin**)LookupPlugin(plug, "plugin_self"); Plugin **plug_self = (Plugin**)LookupPlugin(plug, "plugin_self");
const char *dfhack_version = Version::dfhack_version(); const char *dfhack_version = Version::dfhack_version();
const char *dfhack_git_desc = Version::git_description(); const char *dfhack_git_desc = Version::git_description();
@ -252,10 +305,7 @@ bool Plugin::load(color_ostream &con)
*plug_name, plug_git_desc, dfhack_git_desc); *plug_name, plug_git_desc, dfhack_git_desc);
} }
else else
{
con.printerr("Warning: Plugin %s missing git information\n", *plug_name); con.printerr("Warning: Plugin %s missing git information\n", *plug_name);
plug_git_desc = "unknown";
}
bool *plug_dev = (bool*)LookupPlugin(plug, "plugin_dev"); bool *plug_dev = (bool*)LookupPlugin(plug, "plugin_dev");
if (plug_dev && *plug_dev && getenv("DFHACK_NO_DEV_PLUGINS")) if (plug_dev && *plug_dev && getenv("DFHACK_NO_DEV_PLUGINS"))
{ {
@ -292,7 +342,6 @@ bool Plugin::load(color_ostream &con)
plugin_eval_ruby = (command_result (*)(color_ostream &, const char*)) LookupPlugin(plug, "plugin_eval_ruby"); plugin_eval_ruby = (command_result (*)(color_ostream &, const char*)) LookupPlugin(plug, "plugin_eval_ruby");
plugin_get_exports = (PluginExports* (*)(void)) LookupPlugin(plug, "plugin_get_exports"); plugin_get_exports = (PluginExports* (*)(void)) LookupPlugin(plug, "plugin_get_exports");
index_lua(plug); index_lua(plug);
this->name = *plug_name;
plugin_lib = plug; plugin_lib = plug;
commands.clear(); commands.clear();
if(plugin_init(con,commands) == CR_OK) if(plugin_init(con,commands) == CR_OK)
@ -303,6 +352,7 @@ bool Plugin::load(color_ostream &con)
if ((plugin_onupdate || plugin_enable) && !plugin_is_enabled) if ((plugin_onupdate || plugin_enable) && !plugin_is_enabled)
con.printerr("Plugin %s has no enabled var!\n", name.c_str()); con.printerr("Plugin %s has no enabled var!\n", name.c_str());
fprintf(stderr, "loaded plugin %s; DFHack build %s\n", name.c_str(), plug_git_desc); fprintf(stderr, "loaded plugin %s; DFHack build %s\n", name.c_str(), plug_git_desc);
fflush(stderr);
return true; return true;
} }
else else
@ -364,11 +414,13 @@ bool Plugin::unload(color_ostream &con)
return false; return false;
} }
} }
else if(state == PS_UNLOADED) else if(state == PS_UNLOADED || state == PS_DELETED)
{ {
access->unlock(); access->unlock();
return true; return true;
} }
else if (state == PS_BROKEN)
con.printerr("Plugin %s is broken - cannot be unloaded\n", name.c_str());
access->unlock(); access->unlock();
return false; return false;
} }
@ -741,64 +793,155 @@ bool PluginExports::bind(DFLibrary *lib)
return true; return true;
} }
PluginManager::PluginManager(Core * core) PluginManager::PluginManager(Core * core) : core(core)
{ {
plugin_mutex = new recursive_mutex();
cmdlist_mutex = new mutex(); cmdlist_mutex = new mutex();
ruby = NULL; ruby = NULL;
} }
PluginManager::~PluginManager() PluginManager::~PluginManager()
{ {
for(size_t i = 0; i < all_plugins.size();i++) for (auto it = begin(); it != end(); ++it)
{ {
delete all_plugins[i]; Plugin *p = it->second;
delete p;
} }
all_plugins.clear(); all_plugins.clear();
delete plugin_mutex;
delete cmdlist_mutex; delete cmdlist_mutex;
} }
void PluginManager::init(Core * core) void PluginManager::init()
{ {
#ifdef LINUX_BUILD loadAll();
string path = core->getHackPath() + "plugins/"; }
#ifdef _DARWIN
const string searchstr = ".plug.dylib"; bool PluginManager::addPlugin(string name)
#else {
const string searchstr = ".plug.so"; if (all_plugins.find(name) != all_plugins.end())
#endif {
#else Core::printerr("Plugin already exists: %s\n", name.c_str());
string path = core->getHackPath() + "plugins\\"; return false;
const string searchstr = ".plug.dll"; }
#endif string path = getPluginPath(name);
vector <string> filez; if (!Filesystem::isfile(path))
Filesystem::listdir(path, filez); {
for(size_t i = 0; i < filez.size();i++) Core::printerr("Plugin does not exist: %s\n", name.c_str());
return false;
}
Plugin * p = new Plugin(core, path, name, this);
all_plugins[name] = p;
return true;
}
vector<string> PluginManager::listPlugins()
{
vector<string> results;
vector<string> files;
Filesystem::listdir(getPluginPath(), files);
for (auto file = files.begin(); file != files.end(); ++file)
{ {
if(hasEnding(filez[i],searchstr)) if (hasEnding(*file, plugin_suffix))
{ {
Plugin * p = new Plugin(core, path + filez[i], filez[i], this); string shortname = file->substr(0, file->find(plugin_suffix));
all_plugins.push_back(p); results.push_back(shortname);
// make all plugins load by default (until a proper design emerges).
p->load(core->getConsole());
} }
} }
return results;
}
void PluginManager::refresh()
{
MUTEX_GUARD(plugin_mutex);
auto files = listPlugins();
for (auto f = files.begin(); f != files.end(); ++f)
{
if (!(*this)[*f])
addPlugin(*f);
}
}
bool PluginManager::load (const string &name)
{
MUTEX_GUARD(plugin_mutex);
if (!(*this)[name] && !addPlugin(name))
return false;
Plugin *p = (*this)[name];
if (!p)
{
Core::printerr("Plugin failed to register: %s\n", name.c_str());
return false;
}
return p->load(core->getConsole());
} }
Plugin *PluginManager::getPluginByName (const std::string & name) bool PluginManager::loadAll()
{ {
for(size_t i = 0; i < all_plugins.size(); i++) MUTEX_GUARD(plugin_mutex);
auto files = listPlugins();
bool ok = true;
// load all plugins in hack/plugins
for (auto f = files.begin(); f != files.end(); ++f)
{ {
if(name == all_plugins[i]->name) if (!load(*f))
return all_plugins[i]; ok = false;
} }
return 0; return ok;
}
bool PluginManager::unload (const string &name)
{
MUTEX_GUARD(plugin_mutex);
if (!(*this)[name])
{
Core::printerr("Plugin does not exist: %s\n", name.c_str());
return false;
}
return (*this)[name]->unload(core->getConsole());
}
bool PluginManager::unloadAll()
{
MUTEX_GUARD(plugin_mutex);
bool ok = true;
// only try to unload plugins that are in all_plugins
for (auto it = begin(); it != end(); ++it)
{
if (!unload(it->first))
ok = false;
}
return ok;
}
bool PluginManager::reload (const string &name)
{
// equivalent to "unload(name); load(name);" if plugin is recognized,
// "load(name);" otherwise
MUTEX_GUARD(plugin_mutex);
if (!(*this)[name])
return load(name);
if (!unload(name))
return false;
return load(name);
}
bool PluginManager::reloadAll()
{
MUTEX_GUARD(plugin_mutex);
bool ok = true;
if (!unloadAll())
ok = false;
if (!loadAll())
ok = false;
return ok;
} }
Plugin *PluginManager::getPluginByCommand(const std::string &command) Plugin *PluginManager::getPluginByCommand(const std::string &command)
{ {
tthread::lock_guard<tthread::mutex> lock(*cmdlist_mutex); tthread::lock_guard<tthread::mutex> lock(*cmdlist_mutex);
map <string, Plugin *>::iterator iter = belongs.find(command); map <string, Plugin *>::iterator iter = command_map.find(command);
if (iter != belongs.end()) if (iter != command_map.end())
return iter->second; return iter->second;
else else
return NULL; return NULL;
@ -829,50 +972,70 @@ bool PluginManager::CanInvokeHotkey(const std::string &command, df::viewscreen *
void PluginManager::OnUpdate(color_ostream &out) void PluginManager::OnUpdate(color_ostream &out)
{ {
for(size_t i = 0; i < all_plugins.size(); i++) for (auto it = begin(); it != end(); ++it)
{ it->second->on_update(out);
all_plugins[i]->on_update(out);
}
} }
void PluginManager::OnStateChange(color_ostream &out, state_change_event event) void PluginManager::OnStateChange(color_ostream &out, state_change_event event)
{ {
for(size_t i = 0; i < all_plugins.size(); i++) for (auto it = begin(); it != end(); ++it)
{ it->second->on_state_change(out, event);
all_plugins[i]->on_state_change(out, event);
}
} }
// FIXME: doesn't check name collisions!
void PluginManager::registerCommands( Plugin * p ) void PluginManager::registerCommands( Plugin * p )
{ {
cmdlist_mutex->lock(); cmdlist_mutex->lock();
vector <PluginCommand> & cmds = p->commands; vector <PluginCommand> & cmds = p->commands;
for(size_t i = 0; i < cmds.size();i++) for (size_t i = 0; i < cmds.size();i++)
{ {
std::string name = cmds[i].name; std::string name = cmds[i].name;
if (belongs.find(name) != belongs.end()) if (command_map.find(name) != command_map.end())
{ {
fprintf(stderr, "Plugin %s re-implements command \"%s\" (from plugin %s)\n", core->printerr("Plugin %s re-implements command \"%s\" (from plugin %s)\n",
p->getName().c_str(), name.c_str(), belongs[name]->getName().c_str()); p->getName().c_str(), name.c_str(), command_map[name]->getName().c_str());
continue;
} }
belongs[name] = p; command_map[name] = p;
} }
if (p->plugin_eval_ruby) if (p->plugin_eval_ruby)
ruby = p; ruby = p;
cmdlist_mutex->unlock(); cmdlist_mutex->unlock();
} }
// FIXME: doesn't check name collisions!
void PluginManager::unregisterCommands( Plugin * p ) void PluginManager::unregisterCommands( Plugin * p )
{ {
cmdlist_mutex->lock(); cmdlist_mutex->lock();
vector <PluginCommand> & cmds = p->commands; vector <PluginCommand> & cmds = p->commands;
for(size_t i = 0; i < cmds.size();i++) for(size_t i = 0; i < cmds.size();i++)
{ {
belongs.erase(cmds[i].name); command_map.erase(cmds[i].name);
} }
if (p->plugin_eval_ruby) if (p->plugin_eval_ruby)
ruby = NULL; ruby = NULL;
cmdlist_mutex->unlock(); cmdlist_mutex->unlock();
} }
Plugin *PluginManager::operator[] (std::string name)
{
if (all_plugins.find(name) == all_plugins.end())
{
if (Filesystem::isfile(getPluginPath(name)))
addPlugin(name);
}
return (all_plugins.find(name) != all_plugins.end()) ? all_plugins[name] : NULL;
}
size_t PluginManager::size()
{
return all_plugins.size();
}
std::map<std::string, Plugin*>::iterator PluginManager::begin()
{
return all_plugins.begin();
}
std::map<std::string, Plugin*>::iterator PluginManager::end()
{
return all_plugins.end();
}

@ -30,7 +30,6 @@ distribution.
#include <map> #include <map>
#include "DataDefs.h" #include "DataDefs.h"
#include "PluginManager.h"
#include <lua.h> #include <lua.h>
#include <lauxlib.h> #include <lauxlib.h>
@ -39,7 +38,10 @@ distribution.
* Internal header file of the lua wrapper. * Internal header file of the lua wrapper.
*/ */
namespace DFHack { namespace LuaWrapper {
namespace DFHack {
struct FunctionReg;
namespace LuaWrapper {
struct LuaToken; struct LuaToken;
/** /**
@ -232,7 +234,7 @@ namespace DFHack { namespace LuaWrapper {
/** /**
* Wrap functions and add them to the table on the top of the stack. * Wrap functions and add them to the table on the top of the stack.
*/ */
using DFHack::FunctionReg; typedef DFHack::FunctionReg FunctionReg;
void SetFunctionWrappers(lua_State *state, const FunctionReg *reg); void SetFunctionWrappers(lua_State *state, const FunctionReg *reg);
int method_wrapper_core(lua_State *state, function_identity_base *id); int method_wrapper_core(lua_State *state, function_identity_base *id);

@ -139,22 +139,25 @@ namespace DFHack
struct RefLock; struct RefLock;
struct RefAutolock; struct RefAutolock;
struct RefAutoinc; struct RefAutoinc;
enum plugin_state
{
PS_UNLOADED,
PS_LOADED,
PS_BROKEN,
PS_LOADING,
PS_UNLOADING
};
friend class PluginManager; friend class PluginManager;
friend class RPCService; friend class RPCService;
Plugin(DFHack::Core* core, const std::string& filepath, const std::string& filename, PluginManager * pm); Plugin(DFHack::Core* core, const std::string& filepath,
const std::string &plug_name, PluginManager * pm);
~Plugin(); ~Plugin();
command_result on_update(color_ostream &out); command_result on_update(color_ostream &out);
command_result on_state_change(color_ostream &out, state_change_event event); command_result on_state_change(color_ostream &out, state_change_event event);
void detach_connection(RPCService *svc); void detach_connection(RPCService *svc);
public: public:
enum plugin_state
{
PS_UNLOADED,
PS_LOADED,
PS_BROKEN,
PS_LOADING,
PS_UNLOADING,
PS_DELETED
};
static const char *getStateDescription (plugin_state state);
bool load(color_ostream &out); bool load(color_ostream &out);
bool unload(color_ostream &out); bool unload(color_ostream &out);
bool reload(color_ostream &out); bool reload(color_ostream &out);
@ -183,6 +186,10 @@ namespace DFHack
{ {
return name; return name;
} }
plugin_state getState()
{
return state;
}
void open_lua(lua_State *state, int table); void open_lua(lua_State *state, int table);
@ -196,7 +203,7 @@ namespace DFHack
RefLock * access; RefLock * access;
std::vector <PluginCommand> commands; std::vector <PluginCommand> commands;
std::vector <RPCService*> services; std::vector <RPCService*> services;
std::string filename; std::string path;
std::string name; std::string name;
DFLibrary * plugin_lib; DFLibrary * plugin_lib;
PluginManager * parent; PluginManager * parent;
@ -247,34 +254,44 @@ namespace DFHack
friend class Plugin; friend class Plugin;
PluginManager(Core * core); PluginManager(Core * core);
~PluginManager(); ~PluginManager();
void init(Core* core); void init();
void OnUpdate(color_ostream &out); void OnUpdate(color_ostream &out);
void OnStateChange(color_ostream &out, state_change_event event); void OnStateChange(color_ostream &out, state_change_event event);
void registerCommands( Plugin * p ); void registerCommands( Plugin * p );
void unregisterCommands( Plugin * p ); void unregisterCommands( Plugin * p );
// PUBLIC METHODS // PUBLIC METHODS
public: public:
Plugin *getPluginByName (const std::string & name); // list names of all plugins present in hack/plugins
std::vector<std::string> listPlugins();
// create Plugin instances for any plugins in hack/plugins that aren't present in all_plugins
void refresh();
bool load (const std::string &name);
bool loadAll();
bool unload (const std::string &name);
bool unloadAll();
bool reload (const std::string &name);
bool reloadAll();
Plugin *getPluginByName (const std::string &name) { return (*this)[name]; }
Plugin *getPluginByCommand (const std::string &command); Plugin *getPluginByCommand (const std::string &command);
void *getPluginExports(const std::string &name); void *getPluginExports(const std::string &name);
command_result InvokeCommand(color_ostream &out, const std::string & command, std::vector <std::string> & parameters); command_result InvokeCommand(color_ostream &out, const std::string & command, std::vector <std::string> & parameters);
bool CanInvokeHotkey(const std::string &command, df::viewscreen *top); bool CanInvokeHotkey(const std::string &command, df::viewscreen *top);
Plugin* operator[] (std::size_t index) Plugin* operator[] (const std::string name);
{ std::size_t size();
if(index >= all_plugins.size())
return 0;
return all_plugins[index];
};
std::size_t size()
{
return all_plugins.size();
}
Plugin *ruby; Plugin *ruby;
std::map<std::string, Plugin*>::iterator begin();
std::map<std::string, Plugin*>::iterator end();
// DATA // DATA
private: private:
Core *core;
bool addPlugin(std::string name);
tthread::recursive_mutex * plugin_mutex;
tthread::mutex * cmdlist_mutex; tthread::mutex * cmdlist_mutex;
std::map <std::string, Plugin *> belongs; std::map <std::string, Plugin*> command_map;
std::vector <Plugin *> all_plugins; std::map <std::string, Plugin*> all_plugins;
std::string plugin_path; std::string plugin_path;
}; };
@ -287,10 +304,10 @@ namespace DFHack
} }
}; };
#define DFHACK_PLUGIN_AUX(plugin_name, is_dev) \ #define DFHACK_PLUGIN_AUX(m_plugin_name, is_dev) \
DFhackDataExport const char * name = plugin_name;\ DFhackDataExport const char * plugin_name = m_plugin_name;\
DFhackDataExport const char * version = DFHack::Version::dfhack_version();\ DFhackDataExport const char * plugin_version = DFHack::Version::dfhack_version();\
DFhackDataExport const char * git_description = DFHack::Version::git_description();\ DFhackDataExport const char * plugin_git_description = DFHack::Version::git_description();\
DFhackDataExport Plugin *plugin_self = NULL;\ DFhackDataExport Plugin *plugin_self = NULL;\
std::vector<std::string> _plugin_globals;\ std::vector<std::string> _plugin_globals;\
DFhackDataExport std::vector<std::string>* plugin_globals = &_plugin_globals; \ DFhackDataExport std::vector<std::string>* plugin_globals = &_plugin_globals; \
@ -298,9 +315,9 @@ namespace DFHack
/// You have to include DFHACK_PLUGIN("plugin_name") in every plugin you write - just once. Ideally at the top of the main file. /// You have to include DFHACK_PLUGIN("plugin_name") in every plugin you write - just once. Ideally at the top of the main file.
#ifdef DEV_PLUGIN #ifdef DEV_PLUGIN
#define DFHACK_PLUGIN(plugin_name) DFHACK_PLUGIN_AUX(plugin_name, true) #define DFHACK_PLUGIN(m_plugin_name) DFHACK_PLUGIN_AUX(m_plugin_name, true)
#else #else
#define DFHACK_PLUGIN(plugin_name) DFHACK_PLUGIN_AUX(plugin_name, false) #define DFHACK_PLUGIN(m_plugin_name) DFHACK_PLUGIN_AUX(m_plugin_name, false)
#endif #endif
#define DFHACK_PLUGIN_IS_ENABLED(varname) \ #define DFHACK_PLUGIN_IS_ENABLED(varname) \
@ -330,7 +347,11 @@ namespace DFHack
#define DFHACK_LUA_EVENT(name) { #name, &name##_event } #define DFHACK_LUA_EVENT(name) { #name, &name##_event }
#define DFHACK_LUA_END { NULL, NULL } #define DFHACK_LUA_END { NULL, NULL }
#define REQUIRE_GLOBAL(global_name) \
using df::global::global_name; \ #define REQUIRE_GLOBAL_NO_USE(global_name) \
static int VARIABLE_IS_NOT_USED CONCAT_TOKENS(required_globals_, __LINE__) = \ static int VARIABLE_IS_NOT_USED CONCAT_TOKENS(required_globals_, __LINE__) = \
(plugin_globals->push_back(#global_name), 0); (plugin_globals->push_back(#global_name), 0);
#define REQUIRE_GLOBAL(global_name) \
using df::global::global_name; \
REQUIRE_GLOBAL_NO_USE(global_name)

@ -31,7 +31,7 @@ command_result df_counters (color_ostream &out, vector <string> & parameters)
return CR_OK; return CR_OK;
} }
DFHACK_PLUGIN("probe"); DFHACK_PLUGIN("counters");
DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands) DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <PluginCommand> &commands)
{ {
@ -44,4 +44,4 @@ DFhackCExport command_result plugin_init ( color_ostream &out, std::vector <Plug
DFhackCExport command_result plugin_shutdown ( color_ostream &out ) DFhackCExport command_result plugin_shutdown ( color_ostream &out )
{ {
return CR_OK; return CR_OK;
} }

@ -68,7 +68,7 @@ using namespace df::enums;
using namespace RemoteFortressReader; using namespace RemoteFortressReader;
using namespace std; using namespace std;
DFHACK_PLUGIN("RemoteFortressReader"); DFHACK_PLUGIN("remotefortressreader");
#if DF_VERSION < 40024 #if DF_VERSION < 40024
using namespace df::global; using namespace df::global;
#else #else
@ -1348,4 +1348,4 @@ static command_result GetRegionMaps(color_ostream &stream, const EmptyMessage *i
CopyLocalMap(data, region, regionMap); CopyLocalMap(data, region, regionMap);
} }
return CR_OK; return CR_OK;
} }

@ -343,7 +343,7 @@ static command_result stockflow_cmd(color_ostream &out, vector <string> & parame
desired = true; desired = true;
fast = true; fast = true;
} else if (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?") { } else if (parameters[0] == "usage" || parameters[0] == "help" || parameters[0] == "?") {
out.print("%s: %s\nUsage:\n%s", name, tagline, usage); out.print("%s: %s\nUsage:\n%s", plugin_name, tagline, usage);
return CR_OK; return CR_OK;
} else if (parameters[0] == "list") { } else if (parameters[0] == "list") {
if (!enabled) { if (!enabled) {
@ -417,7 +417,7 @@ DFhackCExport command_result plugin_init(color_ostream &out, std::vector <Plugin
enabled = true; enabled = true;
} }
commands.push_back(PluginCommand(name, tagline, stockflow_cmd, false, usage)); commands.push_back(PluginCommand(plugin_name, tagline, stockflow_cmd, false, usage));
return CR_OK; return CR_OK;
} }