diff --git a/library/Core.cpp b/library/Core.cpp index 04b885fa7..938b875e6 100644 --- a/library/Core.cpp +++ b/library/Core.cpp @@ -51,13 +51,20 @@ using namespace DFHack; #include "dfhack/SDL_fakes/events.h" +#include "dfhack/df/ui.h" +#include "dfhack/df/world.h" +#include "dfhack/df/world_data.h" +#include "dfhack/df/interface.h" +#include "dfhack/df/viewscreen_dwarfmodest.h" + #include #include #include #include #include "tinythread.h" -using namespace tthread; +using namespace tthread; +using namespace df::enums; struct Core::Cond { @@ -145,8 +152,17 @@ void fHKthread(void * iodata) std::string stuff = core->getHotkeyCmd(); // waits on mutex! if(!stuff.empty()) { - vector crap; - command_result cr = plug_mgr->InvokeCommand(stuff, crap, false); + vector args; + cheap_tokenise(stuff, args); + if (args.empty()) { + core->con.printerr("Empty hotkey command.\n"); + continue; + } + + string first = args[0]; + args.erase(args.begin()); + command_result cr = plug_mgr->InvokeCommand(first, args, false); + if(cr == CR_WOULD_BREAK) { core->con.printerr("It isn't possible to run an interactive command outside the console.\n"); @@ -155,53 +171,25 @@ void fHKthread(void * iodata) } } -// A thread function... for the interactive console. -void fIOthread(void * iodata) +static void runInteractiveCommand(Core *core, PluginManager *plug_mgr, int &clueless_counter, const string &command) { - IODATA * iod = ((IODATA*) iodata); - Core * core = iod->core; - PluginManager * plug_mgr = ((IODATA*) iodata)->plug_mgr; - CommandHistory main_history; - main_history.load("dfhack.history"); Console & con = core->con; - if(plug_mgr == 0 || core == 0) - { - con.printerr("Something horrible happened in Core's constructor...\n"); - return; - } - con.print("DFHack is ready. Have a nice day!\n" - "Type in '?' or 'help' for general help, 'ls' to see all commands.\n"); - int clueless_counter = 0; - while (true) + + if (!command.empty()) { - string command = ""; - int ret = con.lineedit("[DFHack]# ",command, main_history); - if(ret == -2) - { - cerr << "Console is shutting down properly." << endl; - return; - } - else if(ret == -1) - { - cerr << "Console caught an unspecified error." << endl; - continue; - } - else if(ret) - { - // a proper, non-empty command was entered - main_history.add(command); - main_history.save("dfhack.history"); - } // cut the input into parts vector parts; cheap_tokenise(command,parts); if(parts.size() == 0) { clueless_counter ++; - continue; + return; } string first = parts[0]; parts.erase(parts.begin()); + + if (first[0] == '#') return; + cerr << "Invoking: " << command << endl; // let's see what we actually got @@ -220,6 +208,7 @@ void fIOthread(void * iodata) " cls - Clear the console.\n" " fpause - Force DF to pause.\n" " die - Force DF to close immediately\n" + " keybinding - Modify bindings of commands to keys\n" "Plugin management (useful for developers):\n" //" belongs COMMAND - Tell which plugin a command belongs to.\n" " plug [PLUGIN|v] - List plugin state and description.\n" @@ -370,6 +359,40 @@ void fIOthread(void * iodata) con.print("%s\n", plug->getName().c_str()); } } + else if(first == "keybinding") + { + if (parts.size() >= 3 && (parts[0] == "set" || parts[0] == "add")) + { + std::string keystr = parts[1]; + if (parts[0] == "set") + core->ClearKeyBindings(keystr); + for (int i = parts.size()-1; i >= 2; i--) + { + if (!core->AddKeyBinding(keystr, parts[i])) { + con.printerr("Invalid key spec: %s\n", keystr.c_str()); + break; + } + } + } + else if (parts.size() >= 2 && parts[0] == "clear") + { + for (unsigned i = 1; i < parts.size(); i++) + { + if (!core->ClearKeyBindings(parts[i])) { + con.printerr("Invalid key spec: %s\n", parts[i].c_str()); + break; + } + } + } + else + { + con << "Usage:" << endl + << " keybinding clear ..." << endl + << " keybinding set \"cmdline\" \"cmdline\"..." << endl + << " keybinding add \"cmdline\" \"cmdline\"..." << endl + << "Later adds, and earlier items within one command have priority." << endl; + } + } else if(first == "fpause") { World * w = core->getWorld(); @@ -410,6 +433,70 @@ void fIOthread(void * iodata) */ } } + } +} + +static void loadInitFile(Core *core, PluginManager *plug_mgr, string fname) +{ + ifstream init(fname); + if (init.bad()) + return; + + int tmp = 0; + string command; + while (getline(init, command)) + { + if (!command.empty()) + runInteractiveCommand(core, plug_mgr, tmp, command); + } +} + +// A thread function... for the interactive console. +void fIOthread(void * iodata) +{ + IODATA * iod = ((IODATA*) iodata); + Core * core = iod->core; + PluginManager * plug_mgr = ((IODATA*) iodata)->plug_mgr; + + CommandHistory main_history; + main_history.load("dfhack.history"); + + Console & con = core->con; + if(plug_mgr == 0 || core == 0) + { + con.printerr("Something horrible happened in Core's constructor...\n"); + return; + } + + loadInitFile(core, plug_mgr, "dfhack.init"); + + con.print("DFHack is ready. Have a nice day!\n" + "Type in '?' or 'help' for general help, 'ls' to see all commands.\n"); + + int clueless_counter = 0; + while (true) + { + string command = ""; + int ret = con.lineedit("[DFHack]# ",command, main_history); + if(ret == -2) + { + cerr << "Console is shutting down properly." << endl; + return; + } + else if(ret == -1) + { + cerr << "Console caught an unspecified error." << endl; + continue; + } + else if(ret) + { + // a proper, non-empty command was entered + main_history.add(command); + main_history.save("dfhack.history"); + } + + runInteractiveCommand(core, plug_mgr, clueless_counter, command); + if(clueless_counter == 3) { con.print("Do 'help' or '?' for the list of available commands.\n"); @@ -434,11 +521,12 @@ Core::Core() StackMutex = 0; core_cond = 0; // set up hotkey capture - memset(hotkey_states,0,sizeof(hotkey_states)); hotkey_set = false; HotkeyMutex = 0; HotkeyCond = 0; misc_data_mutex=0; + last_world_data_ptr = NULL; + top_viewscreen = NULL; }; void Core::fatal (std::string output, bool deactivate) @@ -631,8 +719,36 @@ int Core::Update() if(errorstate) return -1; + // detect if the game was loaded or unloaded in the meantime + void *new_wdata = NULL; + if (df::global::world) { + df::world_data *wdata = df::global::world->world_data; + // when the game is unloaded, world_data isn't deleted, but its contents are + if (wdata && !wdata->sites.empty()) + new_wdata = wdata; + } + + if (new_wdata != last_world_data_ptr) { + last_world_data_ptr = new_wdata; + plug_mgr->OnStateChange(new_wdata ? SC_GAME_LOADED : SC_GAME_UNLOADED); + } + + // detect if the viewscreen changed + if (df::global::gview) + { + df::viewscreen *screen = &df::global::gview->view; + while (screen->child) + screen = screen->child; + if (screen != top_viewscreen) + { + top_viewscreen = screen; + plug_mgr->OnStateChange(SC_VIEWSCREEN_CHANGED); + } + } + // notify all the plugins that a game tick is finished plug_mgr->OnUpdate(); + // wake waiting tools // do not allow more tools to join in while we process stuff here StackMutex->lock(); @@ -721,39 +837,151 @@ int Core::SDL_Event(SDL::Event* ev, int orig_return) if(ev && ev->type == SDL::ET_KEYDOWN || ev->type == SDL::ET_KEYUP) { SDL::KeyboardEvent * ke = (SDL::KeyboardEvent *)ev; - bool shift = ke->ksym.mod & SDL::KMOD_SHIFT; - // consuming F1 .. F8 - int idx = ke->ksym.sym - SDL::K_F1; - if(idx < 0 || idx > 7) - return orig_return; - idx += 8*shift; - // now we have the real index... - if(ke->state == SDL::BTN_PRESSED && !hotkey_states[idx]) + + if(ke->state == SDL::BTN_PRESSED && !hotkey_states[ke->ksym.sym]) { - hotkey_states[idx] = 1; - Gui * g = getGui(); - if(g->hotkeys && g->df_interface && g->df_menu_state) - { - t_viewscreen * ws = g->GetCurrentScreen(); - // FIXME: put hardcoded values into memory.xml - if(ws->getClassName() == "viewscreen_dwarfmodest" && *g->df_menu_state == 0x23) - return orig_return; - else - { - t_hotkey & hotkey = (*g->hotkeys)[idx]; - setHotkeyCmd(hotkey.name); - } - } + hotkey_states[ke->ksym.sym] = true; + + int mod = 0; + if (ke->ksym.mod & SDL::KMOD_SHIFT) mod |= 1; + if (ke->ksym.mod & SDL::KMOD_CTRL) mod |= 2; + if (ke->ksym.mod & SDL::KMOD_ALT) mod |= 4; + + SelectHotkey(ke->ksym.sym, mod); } else if(ke->state == SDL::BTN_RELEASED) { - hotkey_states[idx] = 0; + hotkey_states[ke->ksym.sym] = false; } } return orig_return; // do stuff with the events... } +bool Core::SelectHotkey(int sym, int modifiers) +{ + // Find the topmost viewscreen + if (!df::global::gview || !df::global::ui) + return false; + + df::viewscreen *screen = &df::global::gview->view; + while (screen->child) + screen = screen->child; + + std::string cmd; + + { + tthread::lock_guard lock(*HotkeyMutex); + + // Check the internal keybindings + std::vector &bindings = key_bindings[sym]; + for (int i = bindings.size()-1; i >= 0; --i) { + if (bindings[i].modifiers != modifiers) + continue; + if (!plug_mgr->CanInvokeHotkey(bindings[i].command[0], screen)) + continue; + cmd = bindings[i].cmdline; + break; + } + + if (cmd.empty()) { + // Check the hotkey keybindings + int idx = sym - SDL::K_F1; + if(idx >= 0 && idx < 8) + { + if (modifiers & 1) + idx += 8; + + if (!strict_virtual_cast(screen) || + df::global::ui->main.mode != ui_sidebar_mode::Hotkeys) + { + cmd = df::global::ui->main.hotkeys[idx].name; + } + } + } + } + + if (!cmd.empty()) { + setHotkeyCmd(cmd); + return true; + } + else + return false; +} + +static bool parseKeySpec(std::string keyspec, int *psym, int *pmod) +{ + *pmod = 0; + + // ugh, ugly + for (;;) { + if (keyspec.size() > 6 && keyspec.substr(0, 6) == "Shift-") { + *pmod |= 1; + keyspec = keyspec.substr(6); + } else if (keyspec.size() > 5 && keyspec.substr(0, 5) == "Ctrl-") { + *pmod |= 2; + keyspec = keyspec.substr(5); + } else if (keyspec.size() > 4 && keyspec.substr(0, 4) == "Alt-") { + *pmod |= 4; + keyspec = keyspec.substr(4); + } else + break; + } + + if (keyspec.size() == 1 && keyspec[0] >= 'A' && keyspec[0] <= 'Z') { + *psym = SDL::K_a + (keyspec[0]-'A'); + return true; + } else if (keyspec.size() == 2 && keyspec[0] == 'F' && keyspec[1] >= '1' && keyspec[1] <= '9') { + *psym = SDL::K_F1 + (keyspec[1]-'1'); + return true; + } else + return false; +} + +bool Core::ClearKeyBindings(std::string keyspec) +{ + int sym, mod; + if (!parseKeySpec(keyspec, &sym, &mod)) + return false; + + tthread::lock_guard lock(*HotkeyMutex); + + std::vector &bindings = key_bindings[sym]; + for (int i = bindings.size()-1; i >= 0; --i) { + if (bindings[i].modifiers == mod) + bindings.erase(bindings.begin()+i); + } + + return true; +} + +bool Core::AddKeyBinding(std::string keyspec, std::string cmdline) +{ + int sym; + KeyBinding binding; + if (!parseKeySpec(keyspec, &sym, &binding.modifiers)) + return false; + + cheap_tokenise(cmdline, binding.command); + if (binding.command.empty()) + return false; + + tthread::lock_guard lock(*HotkeyMutex); + + binding.cmdline = cmdline; + key_bindings[sym].push_back(binding); + return true; +} + +bool DFHack::default_hotkey(Core *, df::viewscreen *top) +{ + // Default hotkey guard function + for (;top ;top = top->parent) + if (strict_virtual_cast(top)) + return true; + return false; +} + //////////////// // ClassNamCheck //////////////// diff --git a/library/PluginManager.cpp b/library/PluginManager.cpp index d37c3fbf0..a520575d8 100644 --- a/library/PluginManager.cpp +++ b/library/PluginManager.cpp @@ -27,6 +27,10 @@ distribution. #include "dfhack/Process.h" #include "dfhack/PluginManager.h" #include "dfhack/Console.h" + +#include "dfhack/DataDefs.h" +#include "dfhack/df/viewscreen.h" + using namespace DFHack; #include @@ -135,6 +139,7 @@ Plugin::Plugin(Core * core, const std::string & filepath, const std::string & _f plugin_shutdown = 0; plugin_status = 0; plugin_onupdate = 0; + plugin_onstatechange = 0; state = PS_UNLOADED; access = new RefLock(); } @@ -192,6 +197,7 @@ bool Plugin::load() plugin_status = (command_result (*)(Core *, std::string &)) LookupPlugin(plug, "plugin_status"); plugin_onupdate = (command_result (*)(Core *)) LookupPlugin(plug, "plugin_onupdate"); plugin_shutdown = (command_result (*)(Core *)) LookupPlugin(plug, "plugin_shutdown"); + plugin_onstatechange = (command_result (*)(Core *, state_change_event)) LookupPlugin(plug, "plugin_onstatechange"); //name = _PlugName(); plugin_lib = plug; if(plugin_init(&c,commands) == CR_OK) @@ -271,13 +277,69 @@ command_result Plugin::invoke( std::string & command, std::vector { for (int i = 0; i < commands.size();i++) { - if(commands[i].name == command) + PluginCommand &cmd = commands[i]; + + if(cmd.name == command) { // running interactive things from some other source than the console would break it - if(!interactive_ && commands[i].interactive) + if(!interactive_ && cmd.interactive) cr = CR_WOULD_BREAK; - else - cr = commands[i].function(&c, parameters); + else if (cmd.guard) + { + // Execute hotkey commands in a way where they can + // expect their guard conditions to be matched, + // so as to avoid duplicating checks. + // This means suspending the core beforehand. + CoreSuspender suspend(&c); + df::viewscreen *top = c.getTopViewscreen(); + + if ((cmd.viewscreen_type && !cmd.viewscreen_type->is_instance(top)) + || !cmd.guard(&c, top)) + { + c.con.printerr("Could not invoke %s: unsuitable UI state.\n", command.c_str()); + cr = CR_FAILURE; + } + else + { + cr = cmd.function(&c, parameters); + } + } + else + { + cr = cmd.function(&c, parameters); + } + break; + } + } + } + access->lock_sub(); + return cr; +} + +bool Plugin::can_invoke_hotkey( std::string & command, df::viewscreen *top ) +{ + Core & c = Core::getInstance(); + bool cr = false; + access->lock_add(); + if(state == PS_LOADED) + { + for (int i = 0; i < commands.size();i++) + { + PluginCommand &cmd = commands[i]; + + if(cmd.name == command) + { + if (cmd.interactive) + cr = false; + else if (cmd.guard) + { + cr = (!cmd.viewscreen_type || cmd.viewscreen_type->is_instance(top)) + && cmd.guard(&c, top); + } + else + { + cr = default_hotkey(&c, top); + } break; } } @@ -299,6 +361,19 @@ command_result Plugin::on_update() return cr; } +command_result Plugin::on_state_change(state_change_event event) +{ + Core & c = Core::getInstance(); + command_result cr = CR_NOT_IMPLEMENTED; + access->lock_add(); + if(state == PS_LOADED && plugin_onstatechange) + { + cr = plugin_onstatechange(&c, event); + } + access->lock_sub(); + return cr; +} + Plugin::plugin_state Plugin::getState() const { return state; @@ -348,19 +423,27 @@ Plugin *PluginManager::getPluginByName (const std::string & name) return 0; } +Plugin *PluginManager::getPluginByCommand(const std::string &command) +{ + tthread::lock_guard lock(*cmdlist_mutex); + map ::iterator iter = belongs.find(command); + if (iter != belongs.end()) + return iter->second; + else + return NULL; +} + // FIXME: handle name collisions... command_result PluginManager::InvokeCommand( std::string & command, std::vector & parameters, bool interactive) { - command_result cr = CR_NOT_IMPLEMENTED; - Core * c = &Core::getInstance(); - cmdlist_mutex->lock(); - map ::iterator iter = belongs.find(command); - if(iter != belongs.end()) - { - cr = iter->second->invoke(command, parameters, interactive); - } - cmdlist_mutex->unlock(); - return cr; + Plugin *plugin = getPluginByCommand(command); + return plugin ? plugin->invoke(command, parameters, interactive) : CR_NOT_IMPLEMENTED; +} + +bool PluginManager::CanInvokeHotkey(std::string &command, df::viewscreen *top) +{ + Plugin *plugin = getPluginByCommand(command); + return plugin ? plugin->can_invoke_hotkey(command, top) : false; } void PluginManager::OnUpdate( void ) @@ -370,6 +453,15 @@ void PluginManager::OnUpdate( void ) all_plugins[i]->on_update(); } } + +void PluginManager::OnStateChange( state_change_event event ) +{ + for(int i = 0; i < all_plugins.size(); i++) + { + all_plugins[i]->on_state_change(event); + } +} + // FIXME: doesn't check name collisions! void PluginManager::registerCommands( Plugin * p ) { diff --git a/library/include/dfhack/Core.h b/library/include/dfhack/Core.h index 64eae5d3e..427cdf048 100644 --- a/library/include/dfhack/Core.h +++ b/library/include/dfhack/Core.h @@ -43,6 +43,11 @@ namespace tthread class thread; } +namespace df +{ + struct viewscreen; +} + namespace DFHack { class Process; @@ -134,6 +139,12 @@ namespace DFHack /// returns a named pointer. void *GetData(std::string key); + bool ClearKeyBindings(std::string keyspec); + bool AddKeyBinding(std::string keyspec, std::string cmdline); + + bool isWorldLoaded() { return (last_world_data_ptr != NULL); } + df::viewscreen *getTopViewscreen() { return top_viewscreen; } + DFHack::Process * p; DFHack::VersionInfo * vinfo; DFHack::Console con; @@ -178,12 +189,25 @@ namespace DFHack } s_mods; std::vector allModules; DFHack::PluginManager * plug_mgr; + // hotkey-related stuff - int hotkey_states[16]; + struct KeyBinding { + int modifiers; + std::vector command; + std::string cmdline; + }; + + std::map > key_bindings; + std::map hotkey_states; std::string hotkey_cmd; bool hotkey_set; tthread::mutex * HotkeyMutex; tthread::condition_variable * HotkeyCond; + + bool SelectHotkey(int key, int modifiers); + + void *last_world_data_ptr; // for state change tracking + df::viewscreen *top_viewscreen; // Very important! bool started; diff --git a/library/include/dfhack/PluginManager.h b/library/include/dfhack/PluginManager.h index fef1aea50..3bea5977e 100644 --- a/library/include/dfhack/PluginManager.h +++ b/library/include/dfhack/PluginManager.h @@ -35,10 +35,16 @@ namespace tthread class mutex; class condition_variable; } +namespace df +{ + struct viewscreen; +} namespace DFHack { class Core; class PluginManager; + struct virtual_identity; + enum command_result { CR_WOULD_BREAK = -2, @@ -46,33 +52,48 @@ namespace DFHack CR_FAILURE = 0, CR_OK = 1 }; - struct PluginCommand + enum state_change_event + { + SC_GAME_LOADED, + SC_GAME_UNLOADED, + SC_VIEWSCREEN_CHANGED + }; + struct DFHACK_EXPORT PluginCommand { + typedef command_result (*command_function)(Core *, std::vector &); + typedef bool (*command_hotkey_guard)(Core *, df::viewscreen *); + /// create a command with a name, description, function pointer to its code /// and saying if it needs an interactive terminal /// Most commands shouldn't require an interactive terminal! PluginCommand(const char * _name, const char * _description, - command_result (*function_)(Core *, std::vector &), + command_function function_, bool interactive_ = false ) + : name(_name), description(_description), + function(function_), interactive(interactive_), + guard(NULL), viewscreen_type(NULL) { - name = _name; - description = _description; - function = function_; - interactive = interactive_; } - PluginCommand (const PluginCommand & rhs) + + PluginCommand(const char * _name, + const char * _description, + command_function function_, + command_hotkey_guard guard_, + virtual_identity *viewscreen_type_ = NULL) + : name(_name), description(_description), + function(function_), interactive(false), + guard(guard_), viewscreen_type(viewscreen_type_) { - name = rhs.name; - description = rhs.description; - function = rhs.function; - interactive = rhs.interactive; } + std::string name; std::string description; - command_result (*function)(Core *, std::vector &); + command_function function; bool interactive; + command_hotkey_guard guard; + virtual_identity *viewscreen_type; }; class Plugin { @@ -87,11 +108,13 @@ namespace DFHack Plugin(DFHack::Core* core, const std::string& filepath, const std::string& filename, PluginManager * pm); ~Plugin(); command_result on_update(); + command_result on_state_change(state_change_event event); public: bool load(); bool unload(); bool reload(); command_result invoke( std::string & command, std::vector & parameters, bool interactive ); + bool can_invoke_hotkey( std::string & command, df::viewscreen *top ); plugin_state getState () const; const PluginCommand& operator[] (std::size_t index) const { @@ -117,6 +140,7 @@ namespace DFHack command_result (*plugin_status)(Core *, std::string &); command_result (*plugin_shutdown)(Core *); command_result (*plugin_onupdate)(Core *); + command_result (*plugin_onstatechange)(Core *, state_change_event); }; class DFHACK_EXPORT PluginManager { @@ -126,12 +150,15 @@ namespace DFHack PluginManager(Core * core); ~PluginManager(); void OnUpdate( void ); + void OnStateChange( state_change_event event ); void registerCommands( Plugin * p ); void unregisterCommands( Plugin * p ); // PUBLIC METHODS public: Plugin *getPluginByName (const std::string & name); + Plugin *getPluginByCommand (const std::string &command); command_result InvokeCommand( std::string & command, std::vector & parameters, bool interactive = true ); + bool CanInvokeHotkey(std::string &command, df::viewscreen *top); Plugin* operator[] (std::size_t index) { if(index >= all_plugins.size()) @@ -149,5 +176,7 @@ namespace DFHack std::vector all_plugins; std::string plugin_path; }; + + DFHACK_EXPORT bool default_hotkey(Core *, df::viewscreen *); } diff --git a/library/xml/df.viewscreen.xml b/library/xml/df.viewscreen.xml index cb76db09b..e5816a194 100644 --- a/library/xml/df.viewscreen.xml +++ b/library/xml/df.viewscreen.xml @@ -91,6 +91,11 @@ + + + todo + +