diff --git a/apps/openmw/mwbase/luamanager.hpp b/apps/openmw/mwbase/luamanager.hpp index 5ed751ffed..6c1ce6a27b 100644 --- a/apps/openmw/mwbase/luamanager.hpp +++ b/apps/openmw/mwbase/luamanager.hpp @@ -92,6 +92,8 @@ namespace MWBase // Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script. virtual void reloadAllScripts() = 0; + + virtual void handleConsoleCommand(const std::string& consoleMode, const std::string& command, const MWWorld::Ptr& selectedPtr) = 0; }; } diff --git a/apps/openmw/mwbase/windowmanager.hpp b/apps/openmw/mwbase/windowmanager.hpp index eeb2d6a56b..04f21906a0 100644 --- a/apps/openmw/mwbase/windowmanager.hpp +++ b/apps/openmw/mwbase/windowmanager.hpp @@ -154,6 +154,13 @@ namespace MWBase virtual void updateSpellWindow() = 0; virtual void setConsoleSelectedObject(const MWWorld::Ptr& object) = 0; + virtual void setConsoleMode(const std::string& mode) = 0; + + static constexpr std::string_view sConsoleColor_Default = "#FFFFFF"; + static constexpr std::string_view sConsoleColor_Error = "#FF2222"; + static constexpr std::string_view sConsoleColor_Success = "#FF00FF"; + static constexpr std::string_view sConsoleColor_Info = "#AAAAAA"; + virtual void printToConsole(const std::string& msg, std::string_view color) = 0; /// Set time left for the player to start drowning (update the drowning bar) /// @param time time left to start drowning diff --git a/apps/openmw/mwgui/console.cpp b/apps/openmw/mwgui/console.cpp index a3663b6b9f..aaf36aab59 100644 --- a/apps/openmw/mwgui/console.cpp +++ b/apps/openmw/mwgui/console.cpp @@ -162,25 +162,34 @@ namespace MWGui MyGUI::LayerManager::getInstance().upLayerItem(mMainWidget); } - void Console::print(const std::string &msg, const std::string& color) + void Console::print(const std::string &msg, std::string_view color) { - mHistory->addText(color + MyGUI::TextIterator::toTagsString(msg)); + mHistory->addText(std::string(color) + MyGUI::TextIterator::toTagsString(msg)); } void Console::printOK(const std::string &msg) { - print(msg + "\n", "#FF00FF"); + print(msg + "\n", MWBase::WindowManager::sConsoleColor_Success); } void Console::printError(const std::string &msg) { - print(msg + "\n", "#FF2222"); + print(msg + "\n", MWBase::WindowManager::sConsoleColor_Error); } void Console::execute (const std::string& command) { // Log the command - print("> " + command + "\n"); + if (mConsoleMode.empty()) + print("> " + command + "\n"); + else + print(mConsoleMode + " " + command + "\n"); + + if (!mConsoleMode.empty() || (command.size() >= 3 && std::string_view(command).substr(0, 3) == "lua")) + { + MWBase::Environment::get().getLuaManager()->handleConsoleCommand(mConsoleMode, command, mPtr); + return; + } Compiler::Locals locals; if (!mPtr.isEmpty()) @@ -271,7 +280,7 @@ namespace MWGui } } } - else if(key == MyGUI::KeyCode::Tab) + else if(key == MyGUI::KeyCode::Tab && mConsoleMode.empty()) { std::vector matches; listNames(); @@ -475,7 +484,7 @@ namespace MWGui void Console::onResChange(int width, int height) { - setCoord(10,10, width-10, height/2); + setCoord(10, 10, width-10, height/2); } void Console::updateSelectedObjectPtr(const MWWorld::Ptr& currentPtr, const MWWorld::Ptr& newPtr) @@ -489,23 +498,31 @@ namespace MWGui if (!object.isEmpty()) { if (object == mPtr) - { - setTitle("#{sConsoleTitle}"); - mPtr=MWWorld::Ptr(); - } + mPtr = MWWorld::Ptr(); else - { - setTitle("#{sConsoleTitle} (" + object.getCellRef().getRefId() + ")"); mPtr = object; - } // User clicked on an object. Restore focus to the console command line. MWBase::Environment::get().getWindowManager()->setKeyFocusWidget(mCommandLine); } else - { - setTitle("#{sConsoleTitle}"); mPtr = MWWorld::Ptr(); - } + updateConsoleTitle(); + } + + void Console::updateConsoleTitle() + { + std::string title = "#{sConsoleTitle}"; + if (!mConsoleMode.empty()) + title = mConsoleMode + " " + title; + if (!mPtr.isEmpty()) + title.append(" (" + mPtr.getCellRef().getRefId() + ")"); + setTitle(title); + } + + void Console::setConsoleMode(std::string_view mode) + { + mConsoleMode = std::string(mode); + updateConsoleTitle(); } void Console::onReferenceUnavailable() diff --git a/apps/openmw/mwgui/console.hpp b/apps/openmw/mwgui/console.hpp index 2b8cecbc01..891c4e0ac7 100644 --- a/apps/openmw/mwgui/console.hpp +++ b/apps/openmw/mwgui/console.hpp @@ -9,6 +9,8 @@ #include #include +#include "../mwbase/windowmanager.hpp" + #include "../mwscript/compilercontext.hpp" #include "../mwscript/interpretercontext.hpp" @@ -40,7 +42,7 @@ namespace MWGui void onResChange(int width, int height) override; // Print a message to the console, in specified color. - void print(const std::string &msg, const std::string& color = "#FFFFFF"); + void print(const std::string &msg, std::string_view color = MWBase::WindowManager::sConsoleColor_Default); // These are pre-colored versions that you should use. @@ -60,12 +62,19 @@ namespace MWGui void resetReference () override; + const std::string& getConsoleMode() const { return mConsoleMode; } + void setConsoleMode(std::string_view mode); + protected: void onReferenceUnavailable() override; private: + std::string mConsoleMode; + + void updateConsoleTitle(); + void keyPress(MyGUI::Widget* _sender, MyGUI::KeyCode key, MyGUI::Char _char); diff --git a/apps/openmw/mwgui/windowmanagerimp.cpp b/apps/openmw/mwgui/windowmanagerimp.cpp index 0be8f2c751..23c16082b4 100644 --- a/apps/openmw/mwgui/windowmanagerimp.cpp +++ b/apps/openmw/mwgui/windowmanagerimp.cpp @@ -2069,6 +2069,16 @@ namespace MWGui mConsole->setSelectedObject(object); } + void WindowManager::printToConsole(const std::string& msg, std::string_view color) + { + mConsole->print(msg, color); + } + + void WindowManager::setConsoleMode(const std::string& mode) + { + mConsole->setConsoleMode(mode); + } + std::string WindowManager::correctIconPath(const std::string& path) { return Misc::ResourceHelpers::correctIconPath(path, mResourceSystem->getVFS()); diff --git a/apps/openmw/mwgui/windowmanagerimp.hpp b/apps/openmw/mwgui/windowmanagerimp.hpp index 11d10ab45e..90e8d1b0d7 100644 --- a/apps/openmw/mwgui/windowmanagerimp.hpp +++ b/apps/openmw/mwgui/windowmanagerimp.hpp @@ -195,6 +195,8 @@ namespace MWGui void updateSpellWindow() override; void setConsoleSelectedObject(const MWWorld::Ptr& object) override; + void printToConsole(const std::string& msg, std::string_view color) override; + void setConsoleMode(const std::string& mode) override; /// Set time left for the player to start drowning (update the drowning bar) /// @param time time left to start drowning diff --git a/apps/openmw/mwlua/luamanagerimp.cpp b/apps/openmw/mwlua/luamanagerimp.cpp index aa2d5baa6f..80dfef16cd 100644 --- a/apps/openmw/mwlua/luamanagerimp.cpp +++ b/apps/openmw/mwlua/luamanagerimp.cpp @@ -242,6 +242,9 @@ namespace MWLua for (const std::string& message : mUIMessages) windowManager->messageBox(message); mUIMessages.clear(); + for (auto& [msg, color] : mInGameConsoleMessages) + windowManager->printToConsole(msg, "#" + color.toHex()); + mInGameConsoleMessages.clear(); for (std::unique_ptr& action : mActionQueue) action->safeApply(mWorldView); @@ -256,6 +259,7 @@ namespace MWLua { LuaUi::clearUserInterface(); mUiResourceManager.clear(); + MWBase::Environment::get().getWindowManager()->setConsoleMode(""); mActiveLocalScripts.clear(); mLocalEvents.clear(); mGlobalEvents.clear(); @@ -480,6 +484,7 @@ namespace MWLua Log(Debug::Info) << "Reload Lua"; LuaUi::clearUserInterface(); + MWBase::Environment::get().getWindowManager()->setConsoleMode(""); mUiResourceManager.clear(); mLua.dropScriptCache(); initConfiguration(); @@ -503,6 +508,25 @@ namespace MWLua scripts->receiveEngineEvent(LocalScripts::OnActive()); } + void LuaManager::handleConsoleCommand(const std::string& consoleMode, const std::string& command, const MWWorld::Ptr& selectedPtr) + { + PlayerScripts* playerScripts = nullptr; + if (!mPlayer.isEmpty()) + playerScripts = dynamic_cast(mPlayer.getRefData().getLuaScripts()); + if (!playerScripts) + { + MWBase::Environment::get().getWindowManager()->printToConsole("You must enter a game session to run Lua commands\n", + MWBase::WindowManager::sConsoleColor_Error); + return; + } + sol::object selected = sol::nil; + if (!selectedPtr.isEmpty()) + selected = sol::make_object(mLua.sol(), LObject(getId(selectedPtr), mWorldView.getObjectRegistry())); + if (!playerScripts->consoleCommand(consoleMode, command, selected)) + MWBase::Environment::get().getWindowManager()->printToConsole("No Lua handlers for console\n", + MWBase::WindowManager::sConsoleColor_Error); + } + LuaManager::Action::Action(LuaUtil::LuaState* state) { static const bool luaDebug = Settings::Manager::getBool("lua debug", "Lua"); diff --git a/apps/openmw/mwlua/luamanagerimp.hpp b/apps/openmw/mwlua/luamanagerimp.hpp index fd9ec8b172..a75cac2da9 100644 --- a/apps/openmw/mwlua/luamanagerimp.hpp +++ b/apps/openmw/mwlua/luamanagerimp.hpp @@ -10,6 +10,8 @@ #include +#include + #include "../mwbase/luamanager.hpp" #include "object.hpp" @@ -60,6 +62,10 @@ namespace MWLua // Used only in Lua bindings void addCustomLocalScript(const MWWorld::Ptr&, int scriptId); void addUIMessage(std::string_view message) { mUIMessages.emplace_back(message); } + void addInGameConsoleMessage(const std::string& msg, const Misc::Color& color) + { + mInGameConsoleMessages.push_back({msg, color}); + } // Some changes to the game world can not be done from the scripting thread (because it runs in parallel with OSG Cull), // so we need to queue it and apply from the main thread. All such changes should be implemented as classes inherited @@ -94,6 +100,8 @@ namespace MWLua // Drops script cache and reloads all scripts. Calls `onSave` and `onLoad` for every script. void reloadAllScripts() override; + void handleConsoleCommand(const std::string& consoleMode, const std::string& command, const MWWorld::Ptr& selectedPtr) override; + // Used to call Lua callbacks from C++ void queueCallback(LuaUtil::Callback callback, sol::object arg) { @@ -172,6 +180,7 @@ namespace MWLua std::vector> mActionQueue; std::unique_ptr mTeleportPlayerAction; std::vector mUIMessages; + std::vector> mInGameConsoleMessages; LuaUtil::LuaStorage mGlobalStorage{mLua.sol()}; LuaUtil::LuaStorage mPlayerStorage{mLua.sol()}; diff --git a/apps/openmw/mwlua/playerscripts.hpp b/apps/openmw/mwlua/playerscripts.hpp index 15ab451792..1e909dc72c 100644 --- a/apps/openmw/mwlua/playerscripts.hpp +++ b/apps/openmw/mwlua/playerscripts.hpp @@ -18,7 +18,7 @@ namespace MWLua PlayerScripts(LuaUtil::LuaState* lua, const LObject& obj) : LocalScripts(lua, obj, ESM::LuaScriptCfg::sPlayer) { registerEngineHandlers({ - &mKeyPressHandlers, &mKeyReleaseHandlers, + &mConsoleCommandHandlers, &mKeyPressHandlers, &mKeyReleaseHandlers, &mControllerButtonPressHandlers, &mControllerButtonReleaseHandlers, &mActionHandlers, &mInputUpdateHandlers, &mTouchpadPressed, &mTouchpadReleased, &mTouchpadMoved @@ -59,7 +59,14 @@ namespace MWLua void inputUpdate(float dt) { callEngineHandlers(mInputUpdateHandlers, dt); } + bool consoleCommand(const std::string& consoleMode, const std::string& command, const sol::object& selectedObject) + { + callEngineHandlers(mConsoleCommandHandlers, consoleMode, command, selectedObject); + return !mConsoleCommandHandlers.mList.empty(); + } + private: + EngineHandlerList mConsoleCommandHandlers{"onConsoleCommand"}; EngineHandlerList mKeyPressHandlers{"onKeyPress"}; EngineHandlerList mKeyReleaseHandlers{"onKeyRelease"}; EngineHandlerList mControllerButtonPressHandlers{"onControllerButtonPress"}; diff --git a/apps/openmw/mwlua/uibindings.cpp b/apps/openmw/mwlua/uibindings.cpp index f82dd3db00..84ec98adfb 100644 --- a/apps/openmw/mwlua/uibindings.cpp +++ b/apps/openmw/mwlua/uibindings.cpp @@ -12,6 +12,8 @@ #include "context.hpp" #include "luamanagerimp.hpp" +#include "../mwbase/windowmanager.hpp" + namespace MWLua { namespace @@ -213,6 +215,33 @@ namespace MWLua { luaManager->addUIMessage(message); }; + api["CONSOLE_COLOR"] = LuaUtil::makeReadOnly(context.mLua->tableFromPairs({ + {"Default", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Default.substr(1))}, + {"Error", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Error.substr(1))}, + {"Success", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Success.substr(1))}, + {"Info", Misc::Color::fromHex(MWBase::WindowManager::sConsoleColor_Info.substr(1))}, + })); + api["printToConsole"] = [luaManager=context.mLuaManager](const std::string& message, const Misc::Color& color) + { + luaManager->addInGameConsoleMessage(message + "\n", color); + }; + api["setConsoleMode"] = [luaManager=context.mLuaManager](std::string_view mode) + { + luaManager->addAction( + [mode = std::string(mode)]{ MWBase::Environment::get().getWindowManager()->setConsoleMode(mode); }); + }; + api["setConsoleSelectedObject"] = [luaManager=context.mLuaManager](const sol::object& obj) + { + auto* wm = MWBase::Environment::get().getWindowManager(); + if (obj == sol::nil) + luaManager->addAction([wm]{ wm->setConsoleSelectedObject(MWWorld::Ptr()); }); + else + { + if (!obj.is()) + throw std::runtime_error("Game object expected"); + luaManager->addAction([wm, obj=obj.as()]{ wm->setConsoleSelectedObject(obj.ptr()); }); + } + }; api["content"] = [](const sol::table& table) { return LuaUi::Content(table); diff --git a/docs/source/reference/lua-scripting/engine_handlers.rst b/docs/source/reference/lua-scripting/engine_handlers.rst index adc07d25fe..6f24750736 100644 --- a/docs/source/reference/lua-scripting/engine_handlers.rst +++ b/docs/source/reference/lua-scripting/engine_handlers.rst @@ -70,23 +70,28 @@ Engine handler is a function defined by a script, that can be called by the engi :widths: 20 80 * - onInputUpdate(dt) - - | Called every frame (if the game is not paused) right after processing - | user input. Use it only for latency-critical stuff. + - | Called every frame (if the game is not paused) right after + | processing user input. Use it only for latency-critical stuff. * - onKeyPress(key) - | `Key `_ is pressed. - | Usage example: ``if key.symbol == 'z' and key.withShift then ...`` + | Usage example: + | ``if key.symbol == 'z' and key.withShift then ...`` * - onKeyRelease(key) - | `Key `_ is released. - | Usage example: ``if key.symbol == 'z' and key.withShift then ...`` + | Usage example: + | ``if key.symbol == 'z' and key.withShift then ...`` * - onControllerButtonPress(id) - | A `button `_ on a game controller is pressed. - | Usage example: ``if id == input.CONTROLLER_BUTTON.LeftStick then ...`` + | Usage example: + | ``if id == input.CONTROLLER_BUTTON.LeftStick then ...`` * - onControllerButtonRelease(id) - | A `button `_ on a game controller is released. - | Usage example: ``if id == input.CONTROLLER_BUTTON.LeftStick then ...`` + | Usage example: + | ``if id == input.CONTROLLER_BUTTON.LeftStick then ...`` * - onInputAction(id) - | `Game control `_ is pressed. - | Usage example: ``if id == input.ACTION.ToggleWeapon then ...`` + | Usage example: + | ``if id == input.ACTION.ToggleWeapon then ...`` * - onTouchPress(touchEvent) - | A finger pressed on a touch device. | `Touch event `_. @@ -95,4 +100,9 @@ Engine handler is a function defined by a script, that can be called by the engi | `Touch event `_. * - onTouchMove(touchEvent) - | A finger moved on a touch device. - | `Touch event `_. \ No newline at end of file + | `Touch event `_. + * - | onConsoleCommand( + | mode, command, selectedObject) + - | User entered `command` in in-game console. Called if either + | `mode` is not default or `command` starts with prefix `lua`. + diff --git a/files/builtin_scripts/CMakeLists.txt b/files/builtin_scripts/CMakeLists.txt index 7afcd7f529..1732270527 100644 --- a/files/builtin_scripts/CMakeLists.txt +++ b/files/builtin_scripts/CMakeLists.txt @@ -13,6 +13,9 @@ set(LUA_BUILTIN_FILES scripts/omw/camera.lua scripts/omw/head_bobbing.lua scripts/omw/third_person.lua + scripts/omw/console/player.lua + scripts/omw/console/global.lua + scripts/omw/console/local.lua l10n/Calendar/en.yaml ) diff --git a/files/builtin_scripts/builtin.omwscripts b/files/builtin_scripts/builtin.omwscripts index 2ffe7007f2..e1fbbcdb7c 100644 --- a/files/builtin_scripts/builtin.omwscripts +++ b/files/builtin_scripts/builtin.omwscripts @@ -1,2 +1,5 @@ PLAYER: scripts/omw/camera.lua NPC,CREATURE: scripts/omw/ai.lua +PLAYER: scripts/omw/console/player.lua +GLOBAL: scripts/omw/console/global.lua +CUSTOM: scripts/omw/console/local.lua diff --git a/files/builtin_scripts/openmw_aux/util.lua b/files/builtin_scripts/openmw_aux/util.lua index c442178b5e..4832517418 100644 --- a/files/builtin_scripts/openmw_aux/util.lua +++ b/files/builtin_scripts/openmw_aux/util.lua @@ -1,4 +1,4 @@ -------------------------------------------------------------------------------- +--- -- `openmw_aux.util` defines utility functions that are implemented in Lua rather than in C++. -- Implementation can be found in `resources/vfs/openmw_aux/util.lua`. -- @module util @@ -6,6 +6,27 @@ local aux_util = {} +--- +-- Works like `tostring` but shows also content of tables. +-- @function [parent=#util] deepToString +-- @param #any value The value to conver to string +-- @param #number maxDepth Max depth of tables unpacking (optional, 2 by default) +function aux_util.deepToString(val, level, prefix) + level = (level or 1) - 1 + local ok, iter, t = pcall(function() return pairs(val) end) + if level < 0 or not ok then + return tostring(val) + end + local prefix = prefix or '' + local newPrefix = prefix .. ' ' + local strs = {tostring(val) .. ' {\n'} + for k, v in iter, t do + strs[#strs + 1] = newPrefix .. tostring(k) .. ' = ' .. aux_util.deepToString(v, level, newPrefix) .. ',\n' + end + strs[#strs + 1] = prefix .. '}' + return table.concat(strs) +end + ------------------------------------------------------------------------------- -- Finds the nearest object to the given point in the given list. -- Ignores cells, uses only coordinates. Returns the nearest object, diff --git a/files/builtin_scripts/scripts/omw/console/global.lua b/files/builtin_scripts/scripts/omw/console/global.lua new file mode 100644 index 0000000000..e3b4375b1e --- /dev/null +++ b/files/builtin_scripts/scripts/omw/console/global.lua @@ -0,0 +1,81 @@ +local util = require('openmw.util') + +local player = nil + +local function printToConsole(...) + local strs = {} + for i = 1, select('#', ...) do + strs[i] = tostring(select(i, ...)) + end + player:sendEvent('OMWConsolePrint', table.concat(strs, '\t')) +end + +local function printRes(...) + if select('#', ...) >= 0 then + printToConsole(...) + end +end + +local env = { + I = require('openmw.interfaces'), + util = require('openmw.util'), + storage = require('openmw.storage'), + core = require('openmw.core'), + types = require('openmw.types'), + async = require('openmw.async'), + world = require('openmw.world'), + aux_util = require('openmw_aux.util'), + view = require('openmw_aux.util').deepToString, + print = printToConsole, + exit = function() player:sendEvent('OMWConsoleExit') end, + help = function() player:sendEvent('OMWConsoleHelp') end, +} +env._G = env +setmetatable(env, {__index = _G, __metatable = false}) +_G = nil + +local function executeLuaCode(code) + local fn + local ok, err = pcall(function() fn = util.loadCode('return ' .. code, env) end) + if ok then + ok, err = pcall(function() printRes(fn()) end) + else + ok, err = pcall(function() util.loadCode(code, env)() end) + end + if not ok then + player:sendEvent('OMWConsoleError', err) + end +end + +return { + eventHandlers = { + OMWConsoleEval = function(data) + player = data.player + env.selected = data.selected + executeLuaCode(data.code) + if env.selected ~= data.selected then + local ok, err = pcall(function() player:sendEvent('OMWConsoleSetSelected', env.selected) end) + if not ok then player:sendEvent('OMWConsoleError', err) end + end + end, + OMWConsoleStartLocal = function(data) + player = data.player + ok, err = pcall(function() + if not data.selected:hasScript('scripts/omw/console/local.lua') then + data.selected:addScript('scripts/omw/console/local.lua') + end + end) + if ok then + player:sendEvent('OMWConsoleSetContext', data.selected) + else + player:sendEvent('OMWConsoleError', err) + end + end, + OMWConsoleStopLocal = function(obj) + if obj:hasScript('scripts/omw/console/local.lua') then + obj:removeScript('scripts/omw/console/local.lua') + end + end, + }, +} + diff --git a/files/builtin_scripts/scripts/omw/console/local.lua b/files/builtin_scripts/scripts/omw/console/local.lua new file mode 100644 index 0000000000..adcad3d6cb --- /dev/null +++ b/files/builtin_scripts/scripts/omw/console/local.lua @@ -0,0 +1,71 @@ +local util = require('openmw.util') +local core = require('openmw.core') +local self = require('openmw.self') + +local player = nil + +local function printToConsole(...) + local strs = {} + for i = 1, select('#', ...) do + strs[i] = tostring(select(i, ...)) + end + player:sendEvent('OMWConsolePrint', table.concat(strs, '\t')) +end + +local function printRes(...) + if select('#', ...) >= 0 then + printToConsole(...) + end +end + +local env = { + I = require('openmw.interfaces'), + util = require('openmw.util'), + storage = require('openmw.storage'), + core = require('openmw.core'), + types = require('openmw.types'), + async = require('openmw.async'), + nearby = require('openmw.nearby'), + self = require('openmw.self'), + aux_util = require('openmw_aux.util'), + view = require('openmw_aux.util').deepToString, + print = printToConsole, + exit = function() player:sendEvent('OMWConsoleExit') end, + help = function() player:sendEvent('OMWConsoleHelp') end, +} +env._G = env +setmetatable(env, {__index = _G, __metatable = false}) +_G = nil + +local function executeLuaCode(code) + local fn + local ok, err = pcall(function() fn = util.loadCode('return ' .. code, env) end) + if ok then + ok, err = pcall(function() printRes(fn()) end) + else + ok, err = pcall(function() util.loadCode(code, env)() end) + end + if not ok then + player:sendEvent('OMWConsoleError', err) + end +end + +return { + eventHandlers = { + OMWConsoleEval = function(data) + player = data.player + env.selected = data.selected + executeLuaCode(data.code) + if env.selected ~= data.selected then + local ok, err = pcall(function() player:sendEvent('OMWConsoleSetSelected', env.selected) end) + if not ok then player:sendEvent('OMWConsoleError', err) end + end + end, + }, + engineHandlers = { + onLoad = function() + core.sendGlobalEvent('OMWConsoleStopLocal', self.object) + end, + } +} + diff --git a/files/builtin_scripts/scripts/omw/console/player.lua b/files/builtin_scripts/scripts/omw/console/player.lua new file mode 100644 index 0000000000..fd4309b610 --- /dev/null +++ b/files/builtin_scripts/scripts/omw/console/player.lua @@ -0,0 +1,158 @@ +local ui = require('openmw.ui') +local util = require('openmw.util') +local self = require('openmw.self') +local core = require('openmw.core') + +local function printHelp() + local msg = [[ +This is the built-in Lua interpreter. +help() - print this message +exit() - exit Lua mode +selected - currently selected object (click on any object to change) +view(_G) - print content of the table `_G` (current environment) + standard libraries (math, string, etc.) are loaded by default but not visible in `_G` +view(types, 2) - print table `types` (i.e. `openmw.types`) and its subtables (2 - traversal depth)]] + ui.printToConsole(msg, ui.CONSOLE_COLOR.Info) +end + +local function printToConsole(...) + local strs = {} + for i = 1, select('#', ...) do + strs[i] = tostring(select(i, ...)) + end + return ui.printToConsole(table.concat(strs, '\t'), ui.CONSOLE_COLOR.Info) +end + +local function printRes(...) + if select('#', ...) >= 0 then + printToConsole(...) + end +end + +local currentSelf = nil +local currentMode = '' + +local function exitLuaMode() + currentSelf = nil + currentMode = '' + ui.setConsoleMode('') + ui.printToConsole('Lua mode OFF', ui.CONSOLE_COLOR.Success) +end + +local function setContext(obj) + ui.printToConsole('Lua mode ON, use exit() to return, help() for more info', ui.CONSOLE_COLOR.Success) + if obj == self then + currentMode = 'Lua[Player]' + ui.printToConsole('Context: Player', ui.CONSOLE_COLOR.Success) + elseif obj then + if not obj:isValid() then error('Object not available') end + currentMode = 'Lua['..obj.recordId..']' + ui.printToConsole('Context: Local['..tostring(obj)..']', ui.CONSOLE_COLOR.Success) + else + currentMode = 'Lua[Global]' + ui.printToConsole('Context: Global', ui.CONSOLE_COLOR.Success) + end + currentSelf = obj + ui.setConsoleMode(currentMode) +end + +local function setSelected(obj) + local ok, err = pcall(function() ui.setConsoleSelectedObject(obj) end) + if ok then + ui.printToConsole('Selected object changed', ui.CONSOLE_COLOR.Success) + else + ui.printToConsole(err, ui.CONSOLE_COLOR.Error) + end +end + +local env = { + I = require('openmw.interfaces'), + util = require('openmw.util'), + storage = require('openmw.storage'), + core = require('openmw.core'), + types = require('openmw.types'), + async = require('openmw.async'), + nearby = require('openmw.nearby'), + self = require('openmw.self'), + input = require('openmw.input'), + ui = require('openmw.ui'), + camera = require('openmw.camera'), + aux_util = require('openmw_aux.util'), + view = require('openmw_aux.util').deepToString, + print = printToConsole, + exit = exitLuaMode, + help = printHelp, +} +env._G = env +setmetatable(env, {__index = _G, __metatable = false}) +_G = nil + +local function executeLuaCode(code) + local fn + local ok, err = pcall(function() fn = util.loadCode('return ' .. code, env) end) + if ok then + ok, err = pcall(function() printRes(fn()) end) + else + ok, err = pcall(function() util.loadCode(code, env)() end) + end + if not ok then + ui.printToConsole(err, ui.CONSOLE_COLOR.Error) + end +end + +local function onConsoleCommand(mode, cmd, selectedObject) + env.selected = selectedObject + if mode == '' then + cmd, arg = cmd:lower():match('(%w+) *(%w*)') + if cmd == 'lua' then + if arg == 'player' then + cmd = 'luap' + elseif arg == 'global' then + cmd = 'luag' + elseif arg == 'selected' then + cmd = 'luas' + else + local msg = [[ +Usage: 'lua player' or 'luap' - enter player context + 'lua global' or 'luag' - enter global context + 'lua selected' or 'luas' - enter local context on the selected object]] + ui.printToConsole(msg, ui.CONSOLE_COLOR.Info) + end + end + if cmd == 'luap' or (cmd == 'luas' and selectedObject == self.object) then + setContext(self) + elseif cmd == 'luag' then + setContext() + elseif cmd == 'luas' then + if selectedObject then + core.sendGlobalEvent('OMWConsoleStartLocal', {player=self.object, selected=selectedObject}) + else + ui.printToConsole('No selected object', ui.CONSOLE_COLOR.Error) + end + end + elseif mode == currentMode then + if cmd == 'exit()' then + exitLuaMode() + elseif currentSelf == self then + executeLuaCode(cmd) + if env.selected ~= selectedObject then setSelected(env.selected) end + elseif currentSelf then + currentSelf:sendEvent('OMWConsoleEval', {player=self.object, code=cmd, selected=selectedObject}) + else + core.sendGlobalEvent('OMWConsoleEval', {player=self.object, code=cmd, selected=selectedObject}) + end + end +end + +return { + engineHandlers = {onConsoleCommand = onConsoleCommand}, + eventHandlers = { + OMWConsolePrint = function(msg) ui.printToConsole(tostring(msg), ui.CONSOLE_COLOR.Info) end, + OMWConsoleError = function(msg) ui.printToConsole(tostring(msg), ui.CONSOLE_COLOR.Error) end, + OMWConsoleSetContext = setContext, + OMWConsoleSetSelected = setSelected, + OMWConsoleExit = exitLuaMode, + OMWConsoleHelp = printHelp, + } +} + diff --git a/files/lua_api/openmw/ui.lua b/files/lua_api/openmw/ui.lua index 0ef35a61a5..45adbd8508 100644 --- a/files/lua_api/openmw/ui.lua +++ b/files/lua_api/openmw/ui.lua @@ -38,6 +38,37 @@ -- @function [parent=#ui] showMessage -- @param #string msg +--- +-- Predefined colors for console output +-- @field [parent=#ui] #CONSOLE_COLOR CONSOLE_COLOR + +--- +-- Predefined colors for console output +-- @type CONSOLE_COLOR +-- @field openmw.util#Color Default +-- @field openmw.util#Color Error +-- @field openmw.util#Color Success +-- @field openmw.util#Color Info + +--- +-- Print to the in-game console. +-- @function [parent=#ui] printToConsole +-- @param #string msg +-- @param openmw.util#Color color + +--- +-- Set mode of the in-game console. +-- The mode can be any string, by default is empty. +-- If not empty, then the console doesn't handle mwscript commands and +-- instead passes user input to Lua scripts via `onConsoleCommand` engine handler. +-- @function [parent=#ui] setConsoleMode +-- @param #string mode + +--- +-- Set selected object for console. +-- @function [parent=#ui] setConsoleSelectedObject +-- @param openmw.core#GameObject obj + --- -- Returns the size of the OpenMW window in pixels as a 2D vector. -- @function [parent=#ui] screenSize