/* ScummVM - Graphic Adventure Engine * * ScummVM is the legal property of its developers, whose names * are too numerous to list here. Please refer to the COPYRIGHT * file distributed with this source distribution. * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ /* * This code is based on Broken Sword 2.5 engine * * Copyright (c) Malte Thiesen, Daniel Queteschiner and Michael Elsdoerfer * * Licensed under GNU GPL v2 * */ #include "common/array.h" #include "common/debug-channels.h" #include "sword25/sword25.h" #include "sword25/package/packagemanager.h" #include "sword25/script/luascript.h" #include "sword25/script/luabindhelper.h" #include "sword25/kernel/outputpersistenceblock.h" #include "sword25/kernel/inputpersistenceblock.h" #include "sword25/util/lua/lua.h" #include "sword25/util/lua/lualib.h" #include "sword25/util/lua/lauxlib.h" #include "sword25/util/pluto/pluto.h" namespace Sword25 { LuaScriptEngine::LuaScriptEngine(Kernel *KernelPtr) : ScriptEngine(KernelPtr), _state(0), _pcallErrorhandlerRegistryIndex(0) { } LuaScriptEngine::~LuaScriptEngine() { // Lua de-initialisation if (_state) lua_close(_state); } namespace { int panicCB(lua_State *L) { error("Lua panic. Error message: %s", lua_isnil(L, -1) ? "" : lua_tostring(L, -1)); return 0; } void debugHook(lua_State *L, lua_Debug *ar) { if (!lua_getinfo(L, "Sn", ar)) return; debug("LUA: %s %s: %s %d", ar->namewhat, ar->name, ar->short_src, ar->currentline); } } bool LuaScriptEngine::init() { // Lua-State initialisation, as well as standard libaries initialisation _state = luaL_newstate(); if (!_state || ! registerStandardLibs() || !registerStandardLibExtensions()) { error("Lua could not be initialized."); return false; } // Register panic callback function lua_atpanic(_state, panicCB); // Error handler for lua_pcall calls // The code below contains a local error handler function const char errorHandlerCode[] = "local function ErrorHandler(message) " " return message .. '\\n' .. debug.traceback('', 2) " "end " "return ErrorHandler"; // Compile the code if (luaL_loadbuffer(_state, errorHandlerCode, strlen(errorHandlerCode), "PCALL ERRORHANDLER") != 0) { // An error occurred, so dislay the reason and exit error("Couldn't compile luaL_pcall errorhandler:\n%s", lua_tostring(_state, -1)); lua_pop(_state, 1); return false; } // Running the code, the error handler function sets the top of the stack if (lua_pcall(_state, 0, 1, 0) != 0) { // An error occurred, so dislay the reason and exit error("Couldn't prepare luaL_pcall errorhandler:\n%s", lua_tostring(_state, -1)); lua_pop(_state, 1); return false; } // Place the error handler function in the Lua registry, and remember the index _pcallErrorhandlerRegistryIndex = luaL_ref(_state, LUA_REGISTRYINDEX); // Initialise the Pluto-Persistence library luaopen_pluto(_state); lua_pop(_state, 1); // Initialize debugging callback if (DebugMan.isDebugChannelEnabled(kDebugScript)) { int mask = 0; if ((gDebugLevel & 1) != 0) mask |= LUA_MASKCALL; if ((gDebugLevel & 2) != 0) mask |= LUA_MASKRET; if ((gDebugLevel & 4) != 0) mask |= LUA_MASKLINE; if (mask != 0) lua_sethook(_state, debugHook, mask, 0); } debugC(kDebugScript, "Lua initialized."); return true; } bool LuaScriptEngine::executeFile(const Common::String &fileName) { #ifdef DEBUG int __startStackDepth = lua_gettop(_state); #endif debug(2, "LuaScriptEngine::executeFile(%s)", fileName.c_str()); // Get a pointer to the package manager PackageManager *pPackage = Kernel::getInstance()->getPackage(); assert(pPackage); // File read uint fileSize; byte *fileData = pPackage->getFile(fileName, &fileSize); if (!fileData) { error("Couldn't read \"%s\".", fileName.c_str()); #ifdef DEBUG assert(__startStackDepth == lua_gettop(_state)); #endif return false; } // Run the file content if (!executeBuffer(fileData, fileSize, "@" + pPackage->getAbsolutePath(fileName))) { // Release file buffer delete[] fileData; #ifdef DEBUG assert(__startStackDepth == lua_gettop(_state)); #endif return false; } // Release file buffer delete[] fileData; #ifdef DEBUG assert(__startStackDepth == lua_gettop(_state)); #endif return true; } bool LuaScriptEngine::executeString(const Common::String &code) { return executeBuffer((byte *)code.c_str(), code.size(), "???"); } namespace { void removeForbiddenFunctions(lua_State *L) { static const char *FORBIDDEN_FUNCTIONS[] = { "dofile", 0 }; const char **iterator = FORBIDDEN_FUNCTIONS; while (*iterator) { lua_pushnil(L); lua_setfield(L, LUA_GLOBALSINDEX, *iterator); ++iterator; } } } bool LuaScriptEngine::registerStandardLibs() { luaL_openlibs(_state); removeForbiddenFunctions(_state); return true; } bool LuaScriptEngine::executeBuffer(const byte *data, uint size, const Common::String &name) const { // Compile buffer if (luaL_loadbuffer(_state, (const char *)data, size, name.c_str()) != 0) { error("Couldn't compile \"%s\":\n%s", name.c_str(), lua_tostring(_state, -1)); lua_pop(_state, 1); return false; } // Error handling function to be executed after the function is put on the stack lua_rawgeti(_state, LUA_REGISTRYINDEX, _pcallErrorhandlerRegistryIndex); lua_insert(_state, -2); // Run buffer contents if (lua_pcall(_state, 0, 0, -2) != 0) { error("An error occured while executing \"%s\":\n%s.", name.c_str(), lua_tostring(_state, -1)); lua_pop(_state, 2); return false; } // Remove the error handler function from the stack lua_pop(_state, 1); return true; } void LuaScriptEngine::setCommandLine(const Common::StringArray &commandLineParameters) { lua_newtable(_state); for (size_t i = 0; i < commandLineParameters.size(); ++i) { lua_pushnumber(_state, i + 1); lua_pushstring(_state, commandLineParameters[i].c_str()); lua_settable(_state, -3); } lua_setglobal(_state, "CommandLine"); } namespace { const char *PERMANENTS_TABLE_NAME = "Permanents"; // This array contains the name of global Lua objects that should not be persisted const char *STANDARD_PERMANENTS[] = { "string", "xpcall", "package", "tostring", "print", "os", "unpack", "require", "getfenv", "setmetatable", "next", "assert", "tonumber", "io", "rawequal", "collectgarbage", "getmetatable", "module", "rawset", "warning", "math", "debug", "pcall", "table", "newproxy", "type", "coroutine", "select", "gcinfo", "pairs", "rawget", "loadstring", "ipairs", "_VERSION", "setfenv", "load", "error", "loadfile", "pairs_next", "ipairs_next", "pluto", "Cfg", "Translator", "Persistence", "CommandLine", 0 }; enum PERMANENT_TABLE_TYPE { PTT_PERSIST, PTT_UNPERSIST }; bool pushPermanentsTable(lua_State *L, PERMANENT_TABLE_TYPE tableType) { // Permanents-Table lua_newtable(L); // All standard permanents are inserted into this table uint Index = 0; while (STANDARD_PERMANENTS[Index]) { // Permanents are placed onto the stack; if it does not exist, it is simply ignored lua_getglobal(L, STANDARD_PERMANENTS[Index]); if (!lua_isnil(L, -1)) { // Name of the element as a unique value on the stack lua_pushstring(L, STANDARD_PERMANENTS[Index]); // If it is loaded, then it can be used // In this case, the position of name and object are reversed on the stack if (tableType == PTT_UNPERSIST) lua_insert(L, -2); // Make an entry in the table lua_settable(L, -3); } else { // Pop nil value from stack lua_pop(L, 1); } ++Index; } // All registered C functions to be inserted into the table // BS_LuaBindhelper places in the register a table in which all registered C functions // are stored // Table is put on the stack lua_getfield(L, LUA_REGISTRYINDEX, PERMANENTS_TABLE_NAME); if (!lua_isnil(L, -1)) { // Iterate over all elements of the table lua_pushnil(L); while (lua_next(L, -2) != 0) { // Value and index duplicated on the stack and changed in the sequence lua_pushvalue(L, -1); lua_pushvalue(L, -3); // If it is loaded, then it can be used // In this case, the position of name and object are reversed on the stack if (tableType == PTT_UNPERSIST) lua_insert(L, -2); // Make an entry in the results table lua_settable(L, -6); // Pop value from the stack. The index is then ready for the next call to lua_next() lua_pop(L, 1); } } // Pop the C-Permanents table from the stack lua_pop(L, 1); // coroutine.yield must be registered in the extra-Permanents table because they // are inactive coroutine C functions on the stack // Function coroutine.yield placed on the stack lua_getglobal(L, "coroutine"); lua_pushstring(L, "yield"); lua_gettable(L, -2); // Store coroutine.yield with it's own unique value in the Permanents table lua_pushstring(L, "coroutine.yield"); if (tableType == PTT_UNPERSIST) lua_insert(L, -2); lua_settable(L, -4); // Coroutine table is popped from the stack lua_pop(L, 1); return true; } } namespace { int chunkwriter(lua_State *L, const void *p, size_t sz, void *ud) { Common::Array & chunkData = *reinterpret_cast * >(ud); const byte *buffer = reinterpret_cast(p); while (sz--) chunkData.push_back(*buffer++); return 1; } } bool LuaScriptEngine::persist(OutputPersistenceBlock &writer) { // Empty the Lua stack. pluto_persist() xepects that the stack is empty except for its parameters lua_settop(_state, 0); // Garbage Collection erzwingen. lua_gc(_state, LUA_GCCOLLECT, 0); // Permanents-Table is set on the stack // pluto_persist expects these two items on the Lua stack pushPermanentsTable(_state, PTT_PERSIST); lua_getglobal(_state, "_G"); // Lua persists and stores the data in a Common::Array Common::Array chunkData; pluto_persist(_state, chunkwriter, &chunkData); // Persistenzdaten in den Writer schreiben. writer.writeByteArray(chunkData); // Die beiden Tabellen vom Stack nehmen. lua_pop(_state, 2); return true; } namespace { struct ChunkreaderData { void *BufferPtr; size_t Size; bool BufferReturned; }; const char *chunkreader(lua_State *L, void *ud, size_t *sz) { ChunkreaderData &cd = *reinterpret_cast(ud); if (!cd.BufferReturned) { cd.BufferReturned = true; *sz = cd.Size; return reinterpret_cast(cd.BufferPtr); } else { return 0; } } void clearGlobalTable(lua_State *L, const char **exceptions) { // Iterate over all elements of the global table lua_pushvalue(L, LUA_GLOBALSINDEX); lua_pushnil(L); while (lua_next(L, -2) != 0) { // Now the value and the index of the current element is on the stack // This value does not interest us, so it is popped from the stack lua_pop(L, 1); // Determine whether the item is set to nil, so you want to remove from the global table. // For this will determine whether the element name is a string and is present in // the list of exceptions bool setElementToNil = true; if (lua_isstring(L, -1)) { const char *indexString = lua_tostring(L, -1); const char **exceptionsWalker = exceptions; while (*exceptionsWalker) { if (strcmp(indexString, *exceptionsWalker) == 0) setElementToNil = false; ++exceptionsWalker; } } // If the above test showed that the item should be removed, it is removed by setting the value to nil. if (setElementToNil) { lua_pushvalue(L, -1); lua_pushnil(L); lua_settable(L, LUA_GLOBALSINDEX); } } // Pop the Global table from the stack lua_pop(L, 1); // Perform garbage collection, so that all removed elements are deleted lua_gc(L, LUA_GCCOLLECT, 0); } } bool LuaScriptEngine::unpersist(InputPersistenceBlock &reader) { // Empty the Lua stack. pluto_persist() xepects that the stack is empty except for its parameters lua_settop(_state, 0); // Permanents table is placed on the stack. This has already happened at this point, because // to create the table all permanents must be accessible. This is the case only for the // beginning of the function, because the global table is emptied below pushPermanentsTable(_state, PTT_UNPERSIST); // All items from global table of _G and __METATABLES are removed. // After a garbage collection is performed, and thus all managed objects deleted // __METATABLES is not immediately removed becausen the Metatables are needed // for the finalisers of objects. static const char *clearExceptionsFirstPass[] = { "_G", "__METATABLES", 0 }; clearGlobalTable(_state, clearExceptionsFirstPass); // In the second pass, the Metatables are removed static const char *clearExceptionsSecondPass[] = { "_G", 0 }; clearGlobalTable(_state, clearExceptionsSecondPass); // Persisted Lua data Common::Array chunkData; reader.readByteArray(chunkData); // Chunk-Reader initialisation. It is used with pluto_unpersist to restore read data ChunkreaderData cd; cd.BufferPtr = &chunkData[0]; cd.Size = chunkData.size(); cd.BufferReturned = false; pluto_unpersist(_state, chunkreader, &cd); // Permanents-Table is removed from stack lua_remove(_state, -2); // The read elements in the global table about lua_pushnil(_state); while (lua_next(_state, -2) != 0) { // The referenec to the global table (_G) must not be overwritten, or ticks from Lua total bool isGlobalReference = lua_isstring(_state, -2) && strcmp(lua_tostring(_state, -2), "_G") == 0; if (!isGlobalReference) { lua_pushvalue(_state, -2); lua_pushvalue(_state, -2); lua_settable(_state, LUA_GLOBALSINDEX); } // Pop value from the stack. The index is then ready for the next call to lua_next() lua_pop(_state, 1); } // The table with the loaded data is popped from the stack lua_pop(_state, 1); // Force garbage collection lua_gc(_state, LUA_GCCOLLECT, 0); return true; } } // End of namespace Sword25