From d4d6349f48d01b31f59f94238d6656e3b5d08508 Mon Sep 17 00:00:00 2001 From: Alexander Gavrilov Date: Fri, 4 May 2012 19:47:18 +0400 Subject: [PATCH] Expose builtin commands to dfhack-run, and add lua script support. Move builtin command implementation to Core methods, and fall back to hack/scripts/*.lua for otherwise unrecognized commands. --- library/CMakeLists.txt | 3 + library/Core.cpp | 218 +++++++++++++++++++++------- library/LuaTools.cpp | 20 ++- library/MiscUtils.cpp | 31 ++++ library/PluginManager.cpp | 37 +---- library/RemoteTools.cpp | 2 +- library/include/Core.h | 10 +- library/include/LuaTools.h | 3 + library/include/MiscUtils.h | 3 + library/lua/dfhack.lua | 18 +++ plugins/Dfusion/src/lua_Console.cpp | 3 +- scripts/lua-example.lua | 6 + 12 files changed, 260 insertions(+), 94 deletions(-) create mode 100644 scripts/lua-example.lua diff --git a/library/CMakeLists.txt b/library/CMakeLists.txt index c052df9c8..466175ed4 100644 --- a/library/CMakeLists.txt +++ b/library/CMakeLists.txt @@ -300,6 +300,9 @@ install(DIRECTORY lua/ DESTINATION ${DFHACK_LUA_DESTINATION} FILES_MATCHING PATTERN "*.lua") +install(DIRECTORY ${dfhack_SOURCE_DIR}/scripts + DESTINATION ${DFHACK_DATA_DESTINATION}) + # Unused for so long that it's not even relevant now... if(BUILD_DEVEL) if(WIN32) diff --git a/library/Core.cpp b/library/Core.cpp index 6e1e0986a..60e380598 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -51,6 +51,8 @@ using namespace std; #include "RemoteServer.h" #include "LuaTools.h" +#include "MiscUtils.h" + using namespace DFHack; #include "df/ui.h" @@ -72,8 +74,6 @@ using df::global::init; // FIXME: A lot of code in one file, all doing different things... there's something fishy about it. -static void loadScriptFile(Core *core, PluginManager *plug_mgr, string fname, bool silent); -static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clueless_counter, const string &command); static bool parseKeySpec(std::string keyspec, int *psym, int *pmod); struct Core::Cond @@ -178,21 +178,10 @@ void fHKthread(void * iodata) { color_ostream_proxy out(core->getConsole()); - vector args; - Core::cheap_tokenise(stuff, args); - if (args.empty()) { - out.printerr("Empty hotkey command.\n"); - continue; - } + auto rv = core->runCommand(out, stuff); - string first = args[0]; - args.erase(args.begin()); - command_result cr = plug_mgr->InvokeCommand(out, first, args); - - if(cr == CR_NEEDS_CONSOLE) - { - out.printerr("It isn't possible to run an interactive command outside the console.\n"); - } + if (rv == CR_NOT_IMPLEMENTED) + out.printerr("Invalid hotkey command: '%s'\n", stuff.c_str()); } } } @@ -212,38 +201,122 @@ struct sortable }; }; -static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clueless_counter, const string &command) +static std::string getLuaHelp(std::string path) +{ + ifstream script(path); + + if (script.good()) + { + std::string help; + if (getline(script, help) && + help.substr(0,3) == "-- ") + return help.substr(3); + } + + return "Lua script."; +} + +static std::map listLuaScripts(std::string path) +{ + std::vector files; + getdir(path, files); + + std::map pset; + for (size_t i = 0; i < files.size(); i++) + { + if (hasEnding(files[i], ".lua")) + { + std::string help = getLuaHelp(path + files[i]); + + pset[files[i].substr(0, files[i].size()-4)] = help; + } + } + return pset; +} + +static bool fileExists(std::string path) +{ + ifstream script(path); + return script.good(); +} + +namespace { + struct ScriptArgs { + const string *pcmd; + vector *pargs; + }; +} + +static bool init_run_script(color_ostream &out, lua_State *state, void *info) +{ + auto args = (ScriptArgs*)info; + if (!lua_checkstack(state, args->pargs->size()+10)) + return false; + Lua::PushDFHack(state); + lua_getfield(state, -1, "run_script"); + lua_remove(state, -2); + lua_pushstring(state, args->pcmd->c_str()); + for (size_t i = 0; i < args->pargs->size(); i++) + lua_pushstring(state, (*args->pargs)[i].c_str()); + return true; +} + +static command_result runLuaScript(color_ostream &out, std::string filename, vector &args) +{ + ScriptArgs data; + data.pcmd = &filename; + data.pargs = &args; + +#ifndef LINUX_BUILD + filename = toLower(filename); +#endif + + bool ok = Lua::RunCoreQueryLoop(out, Lua::Core::State, init_run_script, &data); + + return ok ? CR_OK : CR_FAILURE; +} + +command_result Core::runCommand(color_ostream &out, const std::string &command) { - Console & con = core->getConsole(); - if (!command.empty()) { - // cut the input into parts vector parts; Core::cheap_tokenise(command,parts); if(parts.size() == 0) - { - clueless_counter ++; - return; - } + return CR_NOT_IMPLEMENTED; + string first = parts[0]; parts.erase(parts.begin()); - if (first[0] == '#') return; + if (first[0] == '#') + return CR_OK; cerr << "Invoking: " << command << endl; - + + return runCommand(out, first, parts); + } + else + return CR_NOT_IMPLEMENTED; +} + +command_result Core::runCommand(color_ostream &con, const std::string &first, vector &parts) +{ + if (!first.empty()) + { // let's see what we actually got if(first=="help" || first == "?" || first == "man") { if(!parts.size()) { - con.print("This is the DFHack console. You can type commands in and manage DFHack plugins from it.\n" - "Some basic editing capabilities are included (single-line text editing).\n" - "The console also has a command history - you can navigate it with Up and Down keys.\n" - "On Windows, you may have to resize your console window. The appropriate menu is accessible\n" - "by clicking on the program icon in the top bar of the window.\n\n" - "Basic commands:\n" + if (con.is_console()) + { + con.print("This is the DFHack console. You can type commands in and manage DFHack plugins from it.\n" + "Some basic editing capabilities are included (single-line text editing).\n" + "The console also has a command history - you can navigate it with Up and Down keys.\n" + "On Windows, you may have to resize your console window. The appropriate menu is accessible\n" + "by clicking on the program icon in the top bar of the window.\n\n"); + } + con.print("Basic commands:\n" " help|?|man - This text.\n" " help COMMAND - Usage help for the given command.\n" " ls|dir [PLUGIN] - List available commands. Optionally for single plugin.\n" @@ -274,9 +347,15 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue con.reset_color(); if (!pcmd.usage.empty()) con << "Usage:\n" << pcmd.usage << flush; - return; + return CR_OK; } } + auto filename = getHackPath() + "scripts/" + parts[0] + ".lua"; + if (fileExists(filename)) + { + string help = getLuaHelp(filename); + con.print("%s: %s\n", parts[0].c_str(), help.c_str()); + } con.printerr("Unknown command: %s\n", parts[0].c_str()); } else @@ -421,6 +500,13 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue con.print(" %-22s- %s\n",(*iter).name.c_str(), (*iter).description.c_str()); con.reset_color(); } + auto scripts = listLuaScripts(getHackPath() + "scripts/"); + if (!scripts.empty()) + { + con.print("\nscripts:\n"); + for (auto iter = scripts.begin(); iter != scripts.end(); ++iter) + con.print(" %-22s- %s\n", iter->first.c_str(), iter->second.c_str()); + } } } else if(first == "plug") @@ -439,10 +525,10 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue { std::string keystr = parts[1]; if (parts[0] == "set") - core->ClearKeyBindings(keystr); + ClearKeyBindings(keystr); for (int i = parts.size()-1; i >= 2; i--) { - if (!core->AddKeyBinding(keystr, parts[i])) { + if (!AddKeyBinding(keystr, parts[i])) { con.printerr("Invalid key spec: %s\n", keystr.c_str()); break; } @@ -452,7 +538,7 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue { for (size_t i = 1; i < parts.size(); i++) { - if (!core->ClearKeyBindings(parts[i])) { + if (!ClearKeyBindings(parts[i])) { con.printerr("Invalid key spec: %s\n", parts[i].c_str()); break; } @@ -460,7 +546,7 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue } else if (parts.size() == 2 && parts[0] == "list") { - std::vector list = core->ListKeyBindings(parts[1]); + std::vector list = ListKeyBindings(parts[1]); if (list.empty()) con << "No bindings." << endl; for (size_t i = 0; i < list.size(); i++) @@ -478,13 +564,19 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue } else if(first == "fpause") { - World * w = core->getWorld(); + World * w = getWorld(); w->SetPauseState(true); - con.print("The game was forced to pause!"); + con.print("The game was forced to pause!\n"); } else if(first == "cls") { - con.clear(); + if (con.is_console()) + ((Console&)con).clear(); + else + { + con.printerr("No console to clear.\n"); + return CR_NEEDS_CONSOLE; + } } else if(first == "die") { @@ -494,12 +586,13 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue { if(parts.size() == 1) { - loadScriptFile(core, plug_mgr, parts[0], false); + loadScriptFile(con, parts[0], false); } else { con << "Usage:" << endl << " script " << endl; + return CR_WRONG_USAGE; } } else @@ -507,35 +600,44 @@ static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clue command_result res = plug_mgr->InvokeCommand(con, first, parts); if(res == CR_NOT_IMPLEMENTED) { - con.printerr("%s is not a recognized command.\n", first.c_str()); - clueless_counter ++; + auto filename = getHackPath() + "scripts/" + first + ".lua"; + if (fileExists(filename)) + res = runLuaScript(con, filename, parts); + else + con.printerr("%s is not a recognized command.\n", first.c_str()); } + else if (res == CR_NEEDS_CONSOLE) + con.printerr("%s needs interactive console to work.\n", first.c_str()); + return res; } + + return CR_OK; } + + return CR_NOT_IMPLEMENTED; } -static void loadScriptFile(Core *core, PluginManager *plug_mgr, string fname, bool silent) +bool Core::loadScriptFile(color_ostream &out, string fname, bool silent) { if(!silent) - core->getConsole() << "Loading script at " << fname << std::endl; + out << "Loading script at " << fname << std::endl; ifstream script(fname); if (script.good()) { - int tmp = 0; string command; while (getline(script, command)) { if (!command.empty()) - runInteractiveCommand(core, plug_mgr, tmp, command); + runCommand(out, command); } + return true; } else { if(!silent) - core->getConsole().printerr("Error loading script\n"); + out.printerr("Error loading script\n"); + return false; } - - script.close(); } // A thread function... for the interactive console. @@ -555,7 +657,7 @@ void fIOthread(void * iodata) return; } - loadScriptFile(core, plug_mgr, "dfhack.init", true); + core->loadScriptFile(con, "dfhack.init", true); con.print("DFHack is ready. Have a nice day!\n" "Type in '?' or 'help' for general help, 'ls' to see all commands.\n"); @@ -582,7 +684,10 @@ void fIOthread(void * iodata) main_history.save("dfhack.history"); } - runInteractiveCommand(core, plug_mgr, clueless_counter, command); + auto rv = core->runCommand(con, command); + + if (rv == CR_NOT_IMPLEMENTED) + clueless_counter++; if(clueless_counter == 3) { @@ -636,6 +741,15 @@ void Core::fatal (std::string output, bool deactivate) #endif } +std::string Core::getHackPath() +{ +#ifdef LINUX_BUILD + return p->getPath() + "/hack/"; +#else + return p->getPath() + "\\hack\\"; +#endif +} + bool Core::Init() { if(started) diff --git a/library/LuaTools.cpp b/library/LuaTools.cpp index 85e93fbba..17ad0abde 100644 --- a/library/LuaTools.cpp +++ b/library/LuaTools.cpp @@ -706,6 +706,16 @@ static int DFHACK_DFHACK_TOKEN = 0; static int DFHACK_BASE_G_TOKEN = 0; static int DFHACK_REQUIRE_TOKEN = 0; +void Lua::PushDFHack(lua_State *state) +{ + lua_rawgetp(state, LUA_REGISTRYINDEX, &DFHACK_DFHACK_TOKEN); +} + +void Lua::PushBaseGlobals(lua_State *state) +{ + lua_rawgetp(state, LUA_REGISTRYINDEX, &DFHACK_BASE_G_TOKEN); +} + bool DFHack::Lua::PushModule(color_ostream &out, lua_State *state, const char *module) { AssertCoreSuspend(state); @@ -884,13 +894,9 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, bool (*init)(color_ostream&, lua_State*, void*), void *arg) { - if (!out.is_console()) - return false; if (!lua_checkstack(state, 20)) return false; - Console &con = static_cast(out); - lua_State *thread; int rv; std::string prompt; @@ -910,6 +916,10 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, return false; } + // If not interactive, run without coroutine and bail out + if (!out.is_console()) + return SafeCall(out, state, lua_gettop(state)-base-1, 0); + lua_rawgetp(state, LUA_REGISTRYINDEX, &DFHACK_QUERY_COROTABLE_TOKEN); lua_pushvalue(state, base+1); lua_remove(state, base+1); @@ -920,6 +930,8 @@ bool DFHack::Lua::RunCoreQueryLoop(color_ostream &out, lua_State *state, rv = resume_query_loop(out, thread, state, lua_gettop(state)-base, prompt, histfile); } + Console &con = static_cast(out); + while (rv == LUA_YIELD) { if (histfile != histname) diff --git a/library/MiscUtils.cpp b/library/MiscUtils.cpp index c0674686b..65fa21e11 100644 --- a/library/MiscUtils.cpp +++ b/library/MiscUtils.cpp @@ -30,9 +30,12 @@ distribution. #ifndef LINUX_BUILD #include + #include "wdirent.h" #else #include #include + #include + #include #endif #include @@ -125,6 +128,34 @@ std::string toLower(const std::string &str) return rv; } +int getdir(std::string dir, std::vector &files) +{ + DIR *dp; + struct dirent *dirp; + if((dp = opendir(dir.c_str())) == NULL) + { + return errno; + } + while ((dirp = readdir(dp)) != NULL) { + files.push_back(std::string(dirp->d_name)); + } + closedir(dp); + return 0; +} + +bool hasEnding (std::string const &fullString, std::string const &ending) +{ + if (fullString.length() > ending.length()) + { + return (0 == fullString.compare (fullString.length() - ending.length(), ending.length(), ending)); + } + else + { + return false; + } +} + + df::general_ref *DFHack::findRef(std::vector &vec, df::general_ref_type type) { for (int i = vec.size()-1; i >= 0; i--) diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index 837c3e2d5..69b8bfe0d 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -45,41 +45,8 @@ using namespace std; #include "tinythread.h" using namespace tthread; -#ifdef LINUX_BUILD - #include - #include -#else - #include "wdirent.h" -#endif - #include -static int getdir (string dir, vector &files) -{ - DIR *dp; - struct dirent *dirp; - if((dp = opendir(dir.c_str())) == NULL) - { - return errno; - } - while ((dirp = readdir(dp)) != NULL) { - files.push_back(string(dirp->d_name)); - } - closedir(dp); - return 0; -} - -bool hasEnding (std::string const &fullString, std::string const &ending) -{ - if (fullString.length() > ending.length()) - { - return (0 == fullString.compare (fullString.length() - ending.length(), ending.length(), ending)); - } - else - { - return false; - } -} struct Plugin::RefLock { RefLock() @@ -563,10 +530,10 @@ void Plugin::push_function(lua_State *state, LuaFunction *fn) PluginManager::PluginManager(Core * core) { #ifdef LINUX_BUILD - string path = core->p->getPath() + "/hack/plugins/"; + string path = core->getHackPath() + "plugins/"; const string searchstr = ".plug.so"; #else - string path = core->p->getPath() + "\\hack\\plugins\\"; + string path = core->getHackPath() + "plugins\\"; const string searchstr = ".plug.dll"; #endif cmdlist_mutex = new mutex(); diff --git a/library/RemoteTools.cpp b/library/RemoteTools.cpp index b72c0c91d..689c783a8 100644 --- a/library/RemoteTools.cpp +++ b/library/RemoteTools.cpp @@ -711,7 +711,7 @@ command_result CoreService::RunCommand(color_ostream &stream, for (int i = 0; i < in->arguments_size(); i++) args.push_back(in->arguments(i)); - return Core::getInstance().plug_mgr->InvokeCommand(stream, cmd, args); + return Core::getInstance().runCommand(stream, cmd, args); } command_result CoreService::CoreSuspend(color_ostream &stream, const EmptyMessage*, IntMessage *cnt) diff --git a/library/include/Core.h b/library/include/Core.h index 4f9b10c84..fe83715cf 100644 --- a/library/include/Core.h +++ b/library/include/Core.h @@ -35,6 +35,8 @@ distribution. #include "modules/Graphic.h" #include "SDL_events.h" +#include "RemoteClient.h" + struct WINDOW; namespace tthread @@ -117,10 +119,16 @@ namespace DFHack /// returns a named pointer. void *GetData(std::string key); + command_result runCommand(color_ostream &out, const std::string &command, std::vector ¶meters); + command_result runCommand(color_ostream &out, const std::string &command); + bool loadScriptFile(color_ostream &out, std::string fname, bool silent = false); + bool ClearKeyBindings(std::string keyspec); bool AddKeyBinding(std::string keyspec, std::string cmdline); std::vector ListKeyBindings(std::string keyspec); + std::string getHackPath(); + bool isWorldLoaded() { return (last_world_data_ptr != NULL); } bool isMapLoaded() { return (last_local_map_ptr != NULL && last_world_data_ptr != NULL); } @@ -177,7 +185,7 @@ namespace DFHack } s_mods; std::vector allModules; DFHack::PluginManager * plug_mgr; - + // hotkey-related stuff struct KeyBinding { int modifiers; diff --git a/library/include/LuaTools.h b/library/include/LuaTools.h index a6e538d2a..2ffc5c58a 100644 --- a/library/include/LuaTools.h +++ b/library/include/LuaTools.h @@ -48,6 +48,9 @@ namespace DFHack {namespace Lua { */ DFHACK_EXPORT lua_State *Open(color_ostream &out, lua_State *state = NULL); + DFHACK_EXPORT void PushDFHack(lua_State *state); + DFHACK_EXPORT void PushBaseGlobals(lua_State *state); + /** * Load a module using require(). Leaves the stack as is. */ diff --git a/library/include/MiscUtils.h b/library/include/MiscUtils.h index 0cf34c489..08df98d3a 100644 --- a/library/include/MiscUtils.h +++ b/library/include/MiscUtils.h @@ -274,6 +274,9 @@ DFHACK_EXPORT std::string join_strings(const std::string &separator, const std:: DFHACK_EXPORT std::string toUpper(const std::string &str); DFHACK_EXPORT std::string toLower(const std::string &str); +DFHACK_EXPORT int getdir(std::string dir, std::vector &files); +DFHACK_EXPORT bool hasEnding (std::string const &fullString, std::string const &ending); + inline bool bits_match(unsigned required, unsigned ok, unsigned mask) { return (required & mask) == (required & mask & ok); diff --git a/library/lua/dfhack.lua b/library/lua/dfhack.lua index c4c994d2a..c0ba64375 100644 --- a/library/lua/dfhack.lua +++ b/library/lua/dfhack.lua @@ -269,5 +269,23 @@ function dfhack.interpreter(prompt,hfile,env) return true end +-- Command scripts + +dfhack.scripts = dfhack.scripts or {} + +function dfhack.run_script(file,...) + local env = dfhack.scripts[file] + if env == nil then + env = {} + setmetatable(env, { __index = base_env }) + dfhack.scripts[file] = env + end + local f,perr = loadfile(file, 't', env) + if f == nil then + error(perr) + end + return f(...) +end + -- Feed the table back to the require() mechanism. return dfhack diff --git a/plugins/Dfusion/src/lua_Console.cpp b/plugins/Dfusion/src/lua_Console.cpp index 881ba5d21..1d52d6158 100644 --- a/plugins/Dfusion/src/lua_Console.cpp +++ b/plugins/Dfusion/src/lua_Console.cpp @@ -1,6 +1,7 @@ -#include "lua_Console.h" #include "LuaTools.h" +#include "lua_Console.h" + #include //TODO error management. Using lua error? or something other? diff --git a/scripts/lua-example.lua b/scripts/lua-example.lua new file mode 100644 index 000000000..044849ed7 --- /dev/null +++ b/scripts/lua-example.lua @@ -0,0 +1,6 @@ +-- Example of a lua script. + +run_count = (run_count or 0) + 1 + +print('Arguments: ',...) +print('Command called '..run_count..' times.')