diff --git a/apps/openmw/mwlua/luabindings.cpp b/apps/openmw/mwlua/luabindings.cpp index 084de7d5e1..06ba6d5c5a 100644 --- a/apps/openmw/mwlua/luabindings.cpp +++ b/apps/openmw/mwlua/luabindings.cpp @@ -49,7 +49,7 @@ namespace MWLua { auto* lua = context.mLua; sol::table api(lua->sol(), sol::create); - api["API_REVISION"] = 16; + api["API_REVISION"] = 17; api["quit"] = [lua]() { Log(Debug::Warning) << "Quit requested by a Lua script.\n" << lua->debugTraceback(); diff --git a/apps/openmw/mwlua/objectbindings.cpp b/apps/openmw/mwlua/objectbindings.cpp index ef0482f9fe..3ae9035271 100644 --- a/apps/openmw/mwlua/objectbindings.cpp +++ b/apps/openmw/mwlua/objectbindings.cpp @@ -72,7 +72,7 @@ namespace MWLua else throw std::runtime_error("Index out of range"); }; - listT["ipairs"] = [registry](const ListT& list) + listT[sol::meta_function::ipairs] = [registry](const ListT& list) { auto iter = [registry](const ListT& l, int64_t i) -> sol::optional> { diff --git a/apps/openmw_test_suite/lua/test_lua.cpp b/apps/openmw_test_suite/lua/test_lua.cpp index fe3cf14d25..539390f2af 100644 --- a/apps/openmw_test_suite/lua/test_lua.cpp +++ b/apps/openmw_test_suite/lua/test_lua.cpp @@ -39,6 +39,7 @@ return { -- should throw an error incorrectRequire = function() require('counter') end, modifySystemLib = function() math.sin = 5 end, + modifySystemLib2 = function() math.__index.sin = 5 end, rawsetSystemLib = function() rawset(math, 'sin', 5) end, callLoadstring = function() loadstring('print(1)') end, setSqr = function() require('sqrlib').sqr = math.sin end, @@ -119,6 +120,7 @@ return { // but read-only object can not be modified even with rawset EXPECT_ERROR(LuaUtil::call(script["rawsetSystemLib"]), "bad argument #1 to 'rawset' (table expected, got userdata)"); EXPECT_ERROR(LuaUtil::call(script["modifySystemLib"]), "a userdata value"); + EXPECT_ERROR(LuaUtil::call(script["modifySystemLib2"]), "a nil value"); EXPECT_EQ(LuaUtil::getMutableFromReadOnly(LuaUtil::makeReadOnly(script)), script); } diff --git a/components/lua/luastate.cpp b/components/lua/luastate.cpp index bfe9cb513c..bda39d270d 100644 --- a/components/lua/luastate.cpp +++ b/components/lua/luastate.cpp @@ -58,20 +58,41 @@ namespace LuaUtil mLua["math"]["randomseed"] = []{}; mLua["writeToLog"] = [](std::string_view s) { Log(Debug::Level::Info) << s; }; - mLua.script(R"(printToLog = function(name, ...) - local msg = name - for _, v in ipairs({...}) do - msg = msg .. '\t' .. tostring(v) + mLua["cmetatable"] = [](const sol::table& v) -> sol::object { return v[sol::metatable_key]; }; + mLua.script(R"( + local _pairs = pairs + local _ipairs = ipairs + local _tostring = tostring + local _write = writeToLog + local printToLog = function(name, ...) + local msg = name + for _, v in _ipairs({...}) do + msg = msg .. '\t' .. _tostring(v) + end + return _write(msg) end - return writeToLog(msg) - end)"); - mLua.script("printGen = function(name) return function(...) return printToLog(name, ...) end end"); + printGen = function(name) return function(...) return printToLog(name, ...) end end + + local _cmeta = cmetatable + function pairsForReadOnly(v) return _pairs(_cmeta(v).__index) end + function ipairsForReadOnly(v) return _ipairs(_cmeta(v).__index) end + )"); // Some fixes for compatibility between different Lua versions if (mLua["unpack"] == sol::nil) mLua["unpack"] = mLua["table"]["unpack"]; else if (mLua["table"]["unpack"] == sol::nil) mLua["table"]["unpack"] = mLua["unpack"]; + if (LUA_VERSION_NUM <= 501) + { + mLua.script(R"( + local _pairs = pairs + local _ipairs = ipairs + local _cmeta = cmetatable + pairs = function(v) return ((_cmeta(v) or v).__pairs or _pairs)(v) end + ipairs = function(v) return ((_cmeta(v) or v).__ipairs or _ipairs)(v) end + )"); + } mSandboxEnv = sol::table(mLua, sol::create); mSandboxEnv["_VERSION"] = mLua["_VERSION"]; @@ -106,24 +127,22 @@ namespace LuaUtil if (table.is()) return table; // it is already userdata, no sense to wrap it again - lua_State* lua = table.lua_state(); - table[sol::meta_function::index] = table; - lua_newuserdata(lua, 0); - sol::stack::push(lua, std::move(table)); - lua_setmetatable(lua, -2); - return sol::stack::pop(lua); + lua_State* luaState = table.lua_state(); + sol::state_view lua(luaState); + sol::table meta(lua, sol::create); + meta["__index"] = table; + meta["__pairs"] = lua["pairsForReadOnly"]; + meta["__ipairs"] = lua["ipairsForReadOnly"]; + + lua_newuserdata(luaState, 0); + sol::stack::push(luaState, meta); + lua_setmetatable(luaState, -2); + return sol::stack::pop(luaState); } sol::table getMutableFromReadOnly(const sol::userdata& ro) { - lua_State* lua = ro.lua_state(); - sol::stack::push(lua, ro); - int ok = lua_getmetatable(lua, -1); - assert(ok); - (void)ok; - sol::table res = sol::stack::pop(lua); - lua_pop(lua, 1); - return res; + return ro[sol::metatable_key].get()["__index"]; } void LuaState::addCommonPackage(std::string packageName, sol::object package) diff --git a/components/lua/scriptscontainer.cpp b/components/lua/scriptscontainer.cpp index 780d10cebd..11ca28de08 100644 --- a/components/lua/scriptscontainer.cpp +++ b/components/lua/scriptscontainer.cpp @@ -439,10 +439,7 @@ namespace LuaUtil mEventHandlers.clear(); mSimulationTimersQueue.clear(); mGameTimersQueue.clear(); - mPublicInterfaces.clear(); - // Assigned by LuaUtil::makeReadOnly, but `clear` removes it, so we need to assign it again. - mPublicInterfaces[sol::meta_function::index] = mPublicInterfaces; } ScriptsContainer::Script& ScriptsContainer::getScript(int scriptId) diff --git a/docs/source/reference/lua-scripting/overview.rst b/docs/source/reference/lua-scripting/overview.rst index 1bfdf33c42..e38e8948ec 100644 --- a/docs/source/reference/lua-scripting/overview.rst +++ b/docs/source/reference/lua-scripting/overview.rst @@ -4,7 +4,7 @@ Overview of Lua scripting Language and sandboxing ======================= -OpenMW supports scripts written in Lua 5.1. +OpenMW supports scripts written in Lua 5.1 with some extensions (see below) from Lua 5.2. There are no plans to switch to any newer version of the language, because newer versions are not supported by LuaJIT. Here are starting points for learning Lua: @@ -24,11 +24,22 @@ These libraries are loaded automatically and are always available. Allowed `basic functions `__: ``assert``, ``error``, ``ipairs``, ``next``, ``pairs``, ``pcall``, ``print``, ``select``, ``tonumber``, ``tostring``, ``type``, ``unpack``, ``xpcall``, ``rawequal``, ``rawget``, ``rawset``, ``getmetatable``, ``setmetatable``. +Supported Lua 5.2 features: + +- ``goto`` and ``::labels::``; +- hex escapes ``\x3F`` and ``\*`` escape in strings; +- ``math.log(x [,base])``; +- ``string.rep(s, n [,sep])``; +- in ``string.format()``: ``%q`` is reversible, ``%s`` uses ``__tostring``, ``%a`` and ``%A`` are added; +- String matching pattern ``%g``; +- ``__pairs`` and ``__ipairs`` metamethods; +- Function ``table.unpack`` (alias to Lua 5.1 ``unpack``). + Loading libraries with ``require('library_name')`` is allowed, but limited. It works this way: 1. If `library_name` is one of the standard libraries, then return the library. 2. If `library_name` is one of the built-in `API packages`_, then return the package. -3. Otherwise search for a Lua source file with such name in :ref:`data folders `. For example ``require('my_lua_library.something')`` will try to open the file ``my_lua_library/something.lua``. +3. Otherwise search for a Lua source file with such name in :ref:`data folders `. For example ``require('my_lua_library.something')`` will try to open one of the files ``my_lua_library/something.lua`` or ``my_lua_library/something/init.lua``. Loading DLLs and precompiled Lua files is intentionally prohibited for compatibility and security reasons. diff --git a/files/lua_api/openmw/core.lua b/files/lua_api/openmw/core.lua index 07309edca8..e7e657a853 100644 --- a/files/lua_api/openmw/core.lua +++ b/files/lua_api/openmw/core.lua @@ -293,11 +293,6 @@ -- @type ObjectList -- @extends #list<#GameObject> -------------------------------------------------------------------------------- --- Create iterator. --- @function [parent=#ObjectList] ipairs --- @param self - ------------------------------------------------------------------------------- -- Filter list with a Query. -- @function [parent=#ObjectList] select