// celx.cpp // // Copyright (C) 2003-2008, the Celestia Development Team // Original version by Chris Laurel // // Lua script extensions for Celestia. // // 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. #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "celx_internal.h" #include "celx_misc.h" #include "celx_vector.h" #include "celx_rotation.h" #include "celx_position.h" #include "celx_frame.h" #include "celx_phase.h" #include "celx_object.h" #include "celx_observer.h" #include "celx_celestia.h" #include "celx_gl.h" #include "celx_category.h" using namespace Eigen; using namespace std; using celestia::util::GetLogger; const char* CelxLua::ClassNames[] = { "class_celestia", "class_observer", "class_object", "class_vec3", "class_matrix", "class_rotation", "class_position", "class_frame", "class_celscript", "class_font", "class_image", "class_texture", "class_phase", "class_category" }; #define CLASS(i) ClassNames[(i)] // Maximum timeslice a script may run without // returning control to celestia static const double MaxTimeslice = 5.0; // names of callback-functions in Lua: const char* KbdCallback = "celestia_keyboard_callback"; const char* CleanupCallback = "celestia_cleanup_callback"; const char* EventHandlers = "celestia_event_handlers"; const char* KeyHandler = "key"; const char* TickHandler = "tick"; const char* MouseDownHandler = "mousedown"; const char* MouseUpHandler = "mouseup"; #if LUA_VERSION_NUM < 503 int lua_isinteger(lua_State *L, int index) { if (lua_type(L, index) == LUA_TNUMBER) { if (lua_tonumber(L, index) == lua_tointeger(L, index)) return 1; } return 0; } #endif static void openLuaLibrary(lua_State* l, const char* name, lua_CFunction func) { #if LUA_VERSION_NUM >= 502 luaL_requiref(l, name, func, 1); #else lua_pushcfunction(l, func); lua_pushstring(l, name); lua_call(l, 1, 0); #endif } // Push a class name onto the Lua stack void PushClass(lua_State* l, int id) { lua_pushlstring(l, CelxLua::ClassNames[id], strlen(CelxLua::ClassNames[id])); } // Set the class (metatable) of the object on top of the stack void Celx_SetClass(lua_State* l, int id) { PushClass(l, id); lua_rawget(l, LUA_REGISTRYINDEX); if (lua_type(l, -1) != LUA_TTABLE) cout << "Metatable for " << CelxLua::ClassNames[id] << " not found!\n"; if (lua_setmetatable(l, -2) == 0) cout << "Error setting metatable for " << CelxLua::ClassNames[id] << '\n'; } // Initialize the metatable for a class; sets the appropriate registry // entries and __index, leaving the metatable on the stack when done. void Celx_CreateClassMetatable(lua_State* l, int id) { lua_newtable(l); PushClass(l, id); lua_pushvalue(l, -2); lua_rawset(l, LUA_REGISTRYINDEX); // registry.name = metatable lua_pushvalue(l, -1); PushClass(l, id); lua_rawset(l, LUA_REGISTRYINDEX); // registry.metatable = name lua_pushliteral(l, "__index"); lua_pushvalue(l, -2); lua_rawset(l, -3); } // Register a class 'method' in the metatable (assumed to be on top of the stack) void Celx_RegisterMethod(lua_State* l, const char* name, lua_CFunction fn) { lua_pushstring(l, name); lua_pushvalue(l, -2); lua_pushcclosure(l, fn, 1); lua_settable(l, -3); } // Verify that an object at location index on the stack is of the // specified class bool Celx_istype(lua_State* l, int index, int id) { // get registry[metatable] if (!lua_getmetatable(l, index)) return false; lua_rawget(l, LUA_REGISTRYINDEX); if (lua_type(l, -1) != LUA_TSTRING) { cout << "Celx_istype failed! Unregistered class.\n"; lua_pop(l, 1); return false; } const char* classname = lua_tostring(l, -1); lua_pop(l, 1); return classname != nullptr && strcmp(classname, CelxLua::ClassNames[id]) == 0; } // Verify that an object at location index on the stack is of the // specified class and return pointer to userdata void* Celx_CheckUserData(lua_State* l, int index, int id) { if (Celx_istype(l, index, id)) return lua_touserdata(l, index); return nullptr; } // Return the CelestiaCore object stored in the globals table CelestiaCore* getAppCore(lua_State* l, FatalErrors fatalErrors) { lua_pushstring(l, "celestia-appcore"); lua_gettable(l, LUA_REGISTRYINDEX); if (!lua_islightuserdata(l, -1)) { if (fatalErrors == NoErrors) return nullptr; lua_pushstring(l, "internal error: invalid appCore"); lua_error(l); } CelestiaCore* appCore = static_cast(lua_touserdata(l, -1)); lua_pop(l, 1); return appCore; } LuaState::LuaState() : timeout(MaxTimeslice) { state = luaL_newstate(); timer = new Timer(); screenshotCount = 0; } LuaState::~LuaState() { delete timer; if (state != nullptr) lua_close(state); #if 0 if (costate != nullptr) lua_close(costate); #endif } lua_State* LuaState::getState() const { return state; } double LuaState::getTime() const { return timer->getTime(); } // Check if the running script has exceeded its allowed timeslice // and terminate it if it has: static void checkTimeslice(lua_State* l, lua_Debug* /*ar*/) { lua_pushstring(l, "celestia-luastate"); lua_gettable(l, LUA_REGISTRYINDEX); if (!lua_islightuserdata(l, -1)) { lua_pushstring(l, "Internal Error: Invalid table entry in checkTimeslice"); lua_error(l); } LuaState* luastate = static_cast(lua_touserdata(l, -1)); if (luastate == nullptr) { lua_pushstring(l, "Internal Error: Invalid value in checkTimeslice"); lua_error(l); return; } if (luastate->timesliceExpired()) { const char* errormsg = "Timeout: script hasn't returned control to celestia (forgot to call wait()?)"; GetLogger()->error("{}\n", errormsg); lua_pushstring(l, errormsg); lua_error(l); } } // allow the script to perform cleanup void LuaState::cleanup() { if (!costate) return; if (ioMode == Asking) { // Restore renderflags: CelestiaCore* appCore = getAppCore(costate, NoErrors); if (appCore != nullptr) { lua_pushstring(state, "celestia-savedrenderflags"); lua_gettable(state, LUA_REGISTRYINDEX); if (lua_isuserdata(state, -1)) { uint64_t* savedrenderflags = static_cast(lua_touserdata(state, -1)); appCore->getRenderer()->setRenderFlags(*savedrenderflags); // now delete entry: lua_pushstring(state, "celestia-savedrenderflags"); lua_pushnil(state); lua_settable(state, LUA_REGISTRYINDEX); } lua_pop(state,1); } } lua_getglobal(costate, CleanupCallback); if (lua_isnil(costate, -1)) return; timeout = getTime() + 1.0; if (lua_pcall(costate, 0, 0, 0) != 0) { GetLogger()->error("Error while executing cleanup-callback: {}\n", lua_tostring(costate, -1)); } } bool LuaState::createThread() { // Initialize the coroutine which wraps the script if (!(lua_isfunction(state, -1) && !lua_iscfunction(state, -1))) { // Should never happen; we manually set up the stack in C++ assert(0); return false; } costate = lua_newthread(state); if (costate == nullptr) return false; lua_sethook(costate, checkTimeslice, LUA_MASKCOUNT, 1000); lua_pushvalue(state, -2); lua_xmove(state, costate, 1); // move function from L to NL/ alive = true; return true; } string LuaState::getErrorMessage() { if (lua_gettop(state) > 0 && lua_isstring(state, -1)) return lua_tostring(state, -1); return ""; } bool LuaState::timesliceExpired() { if (timeout < getTime()) { // timeslice expired, make every instruction (including pcall) fail: lua_sethook(costate, checkTimeslice, LUA_MASKCOUNT, 1); return true; } return false; } static int resumeLuaThread(lua_State *L, lua_State *co, int narg) { int status; //if (!lua_checkstack(co, narg)) // luaL_error(L, "too many arguments to resume"); lua_xmove(L, co, narg); #if LUA_VERSION_NUM >= 504 int nresults; status = lua_resume(co, nullptr, narg, &nresults); #elif LUA_VERSION_NUM >= 502 status = lua_resume(co, nullptr, narg); #else status = lua_resume(co, narg); #endif if (status == 0 || status == LUA_YIELD) { int nres = lua_gettop(co); //if (!lua_checkstack(L, narg)) // luaL_error(L, "too many results to resume"); lua_xmove(co, L, nres); // move yielded values return nres; } lua_xmove(co, L, 1); // move error message return -1; // error flag } bool LuaState::isAlive() const { return alive; } struct ReadChunkInfo { char* buf; int bufSize; istream* in; }; static const char* readStreamChunk(lua_State* /*unused*/, void* udata, size_t* size) { assert(udata != nullptr); if (udata == nullptr) return nullptr; auto* info = reinterpret_cast(udata); assert(info->buf != nullptr); assert(info->in != nullptr); if (!info->in->good()) { *size = 0; return nullptr; } info->in->read(info->buf, info->bufSize); streamsize nread = info->in->gcount(); *size = (size_t) nread; if (nread == 0) return nullptr; return info->buf; } // Callback for CelestiaCore::charEntered. // Returns true if keypress has been consumed bool LuaState::charEntered(const char* c_p) { if (ioMode == Asking && getTime() > timeout) { int stackTop = lua_gettop(costate); if (c_p[0] == 'y') { openLuaLibrary(costate, LUA_LOADLIBNAME, luaopen_package); openLuaLibrary(costate, LUA_IOLIBNAME, luaopen_io); openLuaLibrary(costate, LUA_OSLIBNAME, luaopen_os); ioMode = IOAllowed; } else { ioMode = IODenied; } CelestiaCore* appCore = getAppCore(costate, NoErrors); if (appCore == nullptr) { GetLogger()->error("ERROR: appCore not found\n"); return true; } appCore->setTextEnterMode(appCore->getTextEnterMode() & ~CelestiaCore::KbPassToScript); appCore->showText("", 0, 0, 0, 0); // Restore renderflags: lua_pushstring(costate, "celestia-savedrenderflags"); lua_gettable(costate, LUA_REGISTRYINDEX); if (lua_isuserdata(costate, -1)) { uint64_t* savedrenderflags = static_cast(lua_touserdata(costate, -1)); appCore->getRenderer()->setRenderFlags(*savedrenderflags); // now delete entry: lua_pushstring(costate, "celestia-savedrenderflags"); lua_pushnil(costate); lua_settable(costate, LUA_REGISTRYINDEX); } else { GetLogger()->warn("Oops, expected savedrenderflags to be userdata\n"); } lua_settop(costate,stackTop); return true; } bool result = true; lua_getglobal(costate, KbdCallback); lua_pushstring(costate, c_p); timeout = getTime() + 1.0; if (lua_pcall(costate, 1, 1, 0) != 0) { GetLogger()->error("Error while executing keyboard-callback: {}\n", lua_tostring(costate, -1)); result = false; } else { if (lua_isboolean(costate, -1)) { result = (lua_toboolean(costate, -1) != 0); } lua_pop(costate, 1); } return result; } // Returns true if a handler is registered for the key bool LuaState::handleKeyEvent(const char* key) { CelestiaCore* appCore = getAppCore(costate, NoErrors); if (appCore == nullptr) return false; // get the registered event table lua_getfield(costate, LUA_REGISTRYINDEX, EventHandlers); if (!lua_istable(costate, -1)) { GetLogger()->error("Missing event handler table"); lua_pop(costate, 1); return false; } bool handled = false; lua_getfield(costate, -1, KeyHandler); if (lua_isfunction(costate, -1)) { lua_remove(costate, -2); // remove the key event table from the stack lua_newtable(costate); lua_pushstring(costate, "char"); lua_pushstring(costate, key); // the default key handler accepts the key name as an argument lua_settable(costate, -3); timeout = getTime() + 1.0; if (lua_pcall(costate, 1, 1, 0) != 0) { GetLogger()->error("Error while executing keyboard callback: {}\n", lua_tostring(costate, -1)); } else { handled = lua_toboolean(costate, -1) == 1 ? true : false; } lua_pop(costate, 1); // pop the return value } else { lua_pop(costate, 2); } return handled; } // Returns true if a handler is registered for the button event bool LuaState::handleMouseButtonEvent(float x, float y, int button, bool down) { CelestiaCore* appCore = getAppCore(costate, NoErrors); if (appCore == nullptr) return false; // get the registered event table lua_getfield(costate, LUA_REGISTRYINDEX, EventHandlers); if (!lua_istable(costate, -1)) { GetLogger()->error("Missing event handler table"); lua_pop(costate, 1); return false; } bool handled = false; lua_getfield(costate, -1, down ? MouseDownHandler : MouseUpHandler); if (lua_isfunction(costate, -1)) { lua_remove(costate, -2); // remove the key event table from the stack lua_newtable(costate); lua_pushstring(costate, "button"); lua_pushnumber(costate, button); lua_settable(costate, -3); lua_pushstring(costate, "x"); lua_pushnumber(costate, x); lua_settable(costate, -3); lua_pushstring(costate, "y"); lua_pushnumber(costate, y); lua_settable(costate, -3); timeout = getTime() + 1.0; if (lua_pcall(costate, 1, 1, 0) != 0) { GetLogger()->error("Error while executing keyboard callback: {}\n", lua_tostring(costate, -1)); } else { handled = lua_toboolean(costate, -1) == 1 ? true : false; } lua_pop(costate, 1); // pop the return value } else { lua_pop(costate, 2); } return handled; } // Returns true if a handler is registered for the tick event bool LuaState::handleTickEvent(double dt) { if (!costate) return true; CelestiaCore* appCore = getAppCore(costate, NoErrors); if (appCore == nullptr) return false; // get the registered event table lua_getfield(costate, LUA_REGISTRYINDEX, EventHandlers); if (!lua_istable(costate, -1)) { GetLogger()->error("Missing event handler table"); lua_pop(costate, 1); return false; } bool handled = false; lua_getfield(costate, -1, TickHandler); if (lua_isfunction(costate, -1)) { lua_remove(costate, -2); // remove the key event table from the stack lua_newtable(costate); lua_pushstring(costate, "dt"); lua_pushnumber(costate, dt); // the default key handler accepts the key name as an argument lua_settable(costate, -3); timeout = getTime() + 1.0; if (lua_pcall(costate, 1, 1, 0) != 0) { GetLogger()->error("Error while executing tick callback: {}\n", lua_tostring(costate, -1)); } else { handled = lua_toboolean(costate, -1) == 1 ? true : false; } lua_pop(costate, 1); // pop the return value } else { lua_pop(costate, 2); } return handled; } int LuaState::loadScript(istream& in, const fs::path& streamname) { char buf[4096]; ReadChunkInfo info; info.buf = buf; info.bufSize = sizeof(buf); info.in = ∈ if (streamname != "string") { lua_pushstring(state, "celestia-scriptpath"); lua_pushstring(state, streamname.string().c_str()); lua_settable(state, LUA_REGISTRYINDEX); } #if LUA_VERSION_NUM >= 502 int status = lua_load(state, readStreamChunk, &info, streamname.string().c_str(), nullptr); #else int status = lua_load(state, readStreamChunk, &info, streamname.string().c_str()); #endif if (status != 0) cout << "Error loading script: " << lua_tostring(state, -1) << '\n'; return status; } int LuaState::loadScript(const std::string& s) { istringstream in(s); return loadScript(in, "string"); } // Resume a thread; if the thread completes, the status is set to !alive int LuaState::resume() { assert(costate != nullptr); if (costate == nullptr) return 0; lua_State* co = lua_tothread(state, -1); //assert(co == costate); // co can be nullptr after error (top stack is errorstring) if (co != costate) return 0; timeout = getTime() + MaxTimeslice; int nArgs = resumeLuaThread(state, co, 0); if (nArgs < 0) { alive = false; const char* errorMessage = lua_tostring(state, -1); if (errorMessage == nullptr) errorMessage = "Unknown script error"; cout << "Error: " << errorMessage << '\n'; CelestiaCore* appCore = getAppCore(co); if (appCore != nullptr) appCore->fatalError(errorMessage); return 1; // just the error string } if (ioMode == Asking) { // timeout now is used to first only display warning, and 1s // later allow response to avoid accidental activation timeout = getTime() + 1.0; } // The thread status is zero if it has terminated normally if (lua_status(co) == 0) alive = false; return nArgs; // arguments from yield } // get current linenumber of script and create // useful error-message void Celx_DoError(lua_State* l, const char* errorMsg) { lua_Debug debug; if (lua_getstack(l, 1, &debug)) { if (lua_getinfo(l, "l", &debug)) { string buf = fmt::sprintf("In line %i: %s", debug.currentline, errorMsg); lua_pushstring(l, buf.c_str()); lua_error(l); } } lua_pushstring(l, errorMsg); lua_error(l); } bool LuaState::tick(double dt) { // Due to the way CelestiaCore::tick is called (at least for KDE), // this method may be entered a second time when we show the error-alerter // Workaround: check if we are alive, return true(!) when we aren't anymore // this way the script isn't deleted after the second enter, but only // when we return from the first enter. OMG. // better Solution: defer showing the error-alterter to CelestiaCore, using // getErrorMessage() if (!isAlive()) return false; if (ioMode == Asking) { CelestiaCore* appCore = getAppCore(costate, NoErrors); if (appCore == nullptr) { GetLogger()->error("ERROR: appCore not found\n"); return true; } lua_pushstring(state, "celestia-savedrenderflags"); lua_gettable(state, LUA_REGISTRYINDEX); if (lua_isnil(state, -1)) { lua_pushstring(state, "celestia-savedrenderflags"); uint64_t* savedrenderflags = static_cast(lua_newuserdata(state, sizeof(int))); *savedrenderflags = appCore->getRenderer()->getRenderFlags(); lua_settable(state, LUA_REGISTRYINDEX); appCore->getRenderer()->setRenderFlags(0); } // now pop result of gettable lua_pop(state, 1); if (getTime() > timeout) { appCore->showText(_("WARNING:\n\nThis script requests permission to read/write files\n" "and execute external programs. Allowing this can be\n" "dangerous.\n" "Do you trust the script and want to allow this?\n\n" "y = yes, ESC = cancel script, any other key = no"), 0, 0, -15, 5, 5); appCore->setTextEnterMode(appCore->getTextEnterMode() | CelestiaCore::KbPassToScript); } else { appCore->showText(_("WARNING:\n\nThis script requests permission to read/write files\n" "and execute external programs. Allowing this can be\n" "dangerous.\n" "Do you trust the script and want to allow this?"), 0, 0, -15, 5, 5); appCore->setTextEnterMode(appCore->getTextEnterMode() & ~CelestiaCore::KbPassToScript); } return false; } if (dt == 0 || scriptAwakenTime > getTime()) return false; int nArgs = resume(); if (!isAlive()) // The script is complete return true; // The script has returned control to us, but it is not completed. lua_State* state = getState(); // The values on the stack indicate what event will wake up the // script. For now, we just support wait() double delay; if (nArgs == 1 && lua_isnumber(state, -1)) delay = lua_tonumber(state, -1); else delay = 0.0; scriptAwakenTime = getTime() + delay; // Clean up the stack lua_pop(state, nArgs); return false; } void LuaState::requestIO() { // the script requested IO, set the mode // so we display the warning during tick // and can request keyboard. We can't do this now // because the script is still active and could // disable keyboard again. if (ioMode == NoIO) { CelestiaCore* appCore = getAppCore(state, AllErrors); string policy = appCore->getConfig()->scriptSystemAccessPolicy; if (policy == "allow") { openLuaLibrary(costate, LUA_LOADLIBNAME, luaopen_package); openLuaLibrary(costate, LUA_IOLIBNAME, luaopen_io); openLuaLibrary(costate, LUA_OSLIBNAME, luaopen_os); ioMode = IOAllowed; } else if (policy == "deny") { ioMode = IODenied; } else { ioMode = Asking; } } } // Check if the number of arguments on the stack matches // the allowed range [minArgs, maxArgs]. Cause an error if not. void Celx_CheckArgs(lua_State* l, int minArgs, int maxArgs, const char* errorMessage) { int argc = lua_gettop(l); if (argc < minArgs || argc > maxArgs) { Celx_DoError(l, errorMessage); } } ObserverFrame::CoordinateSystem parseCoordSys(const string& name) { // 'planetographic' is a deprecated name for bodyfixed, but maintained here // for compatibility with older scripts. if (compareIgnoringCase(name, "universal") == 0) return ObserverFrame::Universal; if (compareIgnoringCase(name, "ecliptic") == 0) return ObserverFrame::Ecliptical; if (compareIgnoringCase(name, "equatorial") == 0) return ObserverFrame::Equatorial; if (compareIgnoringCase(name, "bodyfixed") == 0) return ObserverFrame::BodyFixed; if (compareIgnoringCase(name, "planetographic") == 0) return ObserverFrame::BodyFixed; if (compareIgnoringCase(name, "observer") == 0) return ObserverFrame::ObserverLocal; if (compareIgnoringCase(name, "lock") == 0) return ObserverFrame::PhaseLock; if (compareIgnoringCase(name, "chase") == 0) return ObserverFrame::Chase; return ObserverFrame::Universal; } // Get a pointer to the LuaState-object from the registry: LuaState* getLuaStateObject(lua_State* l) { int stackSize = lua_gettop(l); lua_pushstring(l, "celestia-luastate"); lua_gettable(l, LUA_REGISTRYINDEX); if (!lua_islightuserdata(l, -1)) { Celx_DoError(l, "Internal Error: Invalid table entry for LuaState-pointer"); return 0; } LuaState* luastate_ptr = static_cast(lua_touserdata(l, -1)); if (luastate_ptr == nullptr) { Celx_DoError(l, "Internal Error: Invalid LuaState-pointer"); return 0; } lua_settop(l, stackSize); return luastate_ptr; } // Map the observer to its View. Return nullptr if no view exists // for this observer (anymore). View* getViewByObserver(CelestiaCore* appCore, Observer* obs) { for (const auto view : appCore->views) if (view->observer == obs) return view; return nullptr; } // Fill list with all Observers void getObservers(CelestiaCore* appCore, vector& observerList) { for (const auto view : appCore->views) if (view->type == View::ViewWindow) observerList.push_back(view->observer); } // ==================== Helpers ==================== // safe wrapper for lua_tostring: fatal errors will terminate script by calling // lua_error with errorMsg. const char* Celx_SafeGetString(lua_State* l, int index, FatalErrors fatalErrors, const char* errorMsg) { if (l == nullptr) { GetLogger()->error("Error: LuaState invalid in Celx_SafeGetString\n"); return nullptr; } int argc = lua_gettop(l); if (index < 1 || index > argc) { if (fatalErrors & WrongArgc) Celx_DoError(l, errorMsg); return nullptr; } if (!lua_isstring(l, index)) { if (fatalErrors & WrongType) Celx_DoError(l, errorMsg); return nullptr; } return lua_tostring(l, index); } // safe wrapper for lua_tonumber, c.f. Celx_SafeGetString // Non-fatal errors will return defaultValue. lua_Number Celx_SafeGetNumber(lua_State* l, int index, FatalErrors fatalErrors, const char* errorMsg, lua_Number defaultValue) { if (l == nullptr) { GetLogger()->error("Error: LuaState invalid in Celx_SafeGetNumber\n"); return 0.0; } int argc = lua_gettop(l); if (index < 1 || index > argc) { if (fatalErrors & WrongArgc) { Celx_DoError(l, errorMsg); return 0; } return defaultValue; } if (!lua_isnumber(l, index)) { if (fatalErrors & WrongType) { Celx_DoError(l, errorMsg); return 0; } return defaultValue; } return lua_tonumber(l, index); } // Safe wrapper for lua_tobool, c.f. safeGetString // Non-fatal errors will return defaultValue bool Celx_SafeGetBoolean(lua_State* l, int index, FatalErrors fatalErrors, const char* errorMsg, bool defaultValue) { if (l == nullptr) { GetLogger()->error("Error: LuaState invalid in Celx_SafeGetBoolean\n"); return false; } int argc = lua_gettop(l); if (index < 1 || index > argc) { if (fatalErrors & WrongArgc) { Celx_DoError(l, errorMsg); return 0; } return defaultValue; } if (!lua_isboolean(l, index)) { if (fatalErrors & WrongType) { Celx_DoError(l, errorMsg); return 0; } return defaultValue; } return lua_toboolean(l, index) != 0; } // Add a field to the table on top of the stack void setTable(lua_State* l, const char* field, lua_Number value) { lua_pushstring(l, field); lua_pushnumber(l, value); lua_settable(l, -3); } static void loadLuaLibs(lua_State* state); // ==================== Initialization ==================== bool LuaState::init(CelestiaCore* appCore) { // Import the base, table, string, and math libraries openLuaLibrary(state, "", luaopen_base); openLuaLibrary(state, LUA_MATHLIBNAME, luaopen_math); openLuaLibrary(state, LUA_TABLIBNAME, luaopen_table); openLuaLibrary(state, LUA_STRLIBNAME, luaopen_string); #if LUA_VERSION_NUM >= 502 openLuaLibrary(state, LUA_COLIBNAME, luaopen_coroutine); #endif // Make the package library, except the loadlib function, available // for celx regardless of script system access policy. allowLuaPackageAccess(); // Add an easy to use wait function, so that script writers can // live in ignorance of coroutines. There will probably be a significant // library of useful functions that can be defined purely in Lua. // At that point, we'll want something a bit more robust than just // parsing the whole text of the library every time a script is launched if (loadScript("wait = function(x) coroutine.yield(x) end") != 0) return false; // Execute the script fragment to define the wait function if (lua_pcall(state, 0, 0, 0) != 0) { cout << "Error running script initialization fragment.\n"; return false; } lua_pushnumber(state, (lua_Number)KM_PER_LY/1e6); lua_setglobal(state, "KM_PER_MICROLY"); loadLuaLibs(state); // Create the celestia object celestia_new(state, appCore); lua_setglobal(state, "celestia"); // add reference to appCore in the registry lua_pushstring(state, "celestia-appcore"); lua_pushlightuserdata(state, static_cast(appCore)); lua_settable(state, LUA_REGISTRYINDEX); // add a reference to the LuaState-object in the registry lua_pushstring(state, "celestia-luastate"); lua_pushlightuserdata(state, static_cast(this)); lua_settable(state, LUA_REGISTRYINDEX); lua_pushstring(state, EventHandlers); lua_newtable(state); lua_settable(state, LUA_REGISTRYINDEX); #if 0 lua_getglobal(state, "dofile"); // function "dofile" on stack lua_pushstring(state, "luainit.celx"); // parameter if (lua_pcall(state, 1, 0, 0) != 0) // execute it { // copy string?! const char* errorMessage = lua_tostring(state, -1); cout << errorMessage << '\n'; cout.flush(); appCore->fatalError(errorMessage); return false; } #endif return true; } void LuaState::setLuaPath(const string& s) { lua_getglobal(state, "package"); lua_pushstring(state, s.c_str()); lua_setfield(state, -2, "path"); lua_pop(state, 1); } // ==================== Load Libraries ================================================ static void loadLuaLibs(lua_State* state) { openLuaLibrary(state, LUA_DBLIBNAME, luaopen_debug); CreateObjectMetaTable(state); CreateObserverMetaTable(state); CreateCelestiaMetaTable(state); CreatePositionMetaTable(state); CreateVectorMetaTable(state); CreateRotationMetaTable(state); CreateFrameMetaTable(state); CreatePhaseMetaTable(state); CreateCelscriptMetaTable(state); CreateFontMetaTable(state); CreateImageMetaTable(state); CreateTextureMetaTable(state); CreateCategoryMetaTable(state); ExtendCelestiaMetaTable(state); ExtendObjectMetaTable(state); #ifndef GL_ES LoadLuaGraphicsLibrary(state); #endif } void LuaState::allowSystemAccess() { openLuaLibrary(state, LUA_LOADLIBNAME, luaopen_package); openLuaLibrary(state, LUA_IOLIBNAME, luaopen_io); openLuaLibrary(state, LUA_OSLIBNAME, luaopen_os); ioMode = IOAllowed; } // Permit access to the package library, but prohibit use of the loadlib // function. void LuaState::allowLuaPackageAccess() { openLuaLibrary(state, LUA_LOADLIBNAME, luaopen_package); // Disallow loadlib lua_getglobal(state, "package"); lua_pushnil(state); lua_setfield(state, -2, "loadlib"); lua_pop(state, 1); } // ==================== Lua Hook Methods ================================================ void LuaState::setLuaHookEventHandlerEnabled(bool enable) { eventHandlerEnabled = enable; } bool LuaState::callLuaHook(void* obj, const char* method) { if (!eventHandlerEnabled) return false; lua_pushlightuserdata(costate, obj); lua_gettable(costate, LUA_REGISTRYINDEX); if (!lua_istable(costate, -1)) { lua_pop(costate, 1); return false; } bool handled = false; lua_pushstring(costate, method); lua_gettable(costate, -2); if (lua_isfunction(costate, -1)) { lua_pushvalue(costate, -2); // push the Lua object the stack lua_remove(costate, -3); // remove the Lua object from the stack timeout = getTime() + 1.0; if (lua_pcall(costate, 1, 1, 0) != 0) { GetLogger()->error("Error while executing Lua Hook: {}\n", lua_tostring(costate, -1)); } else { handled = lua_toboolean(costate, -1) == 1 ? true : false; } lua_pop(costate, 1); // pop the return value } else { lua_pop(costate, 2); } return handled; } bool LuaState::callLuaHook(void* obj, const char* method, const char* keyName) { if (!eventHandlerEnabled) return false; lua_pushlightuserdata(costate, obj); lua_gettable(costate, LUA_REGISTRYINDEX); if (!lua_istable(costate, -1)) { lua_pop(costate, 1); return false; } bool handled = false; lua_pushstring(costate, method); lua_gettable(costate, -2); if (lua_isfunction(costate, -1)) { lua_pushvalue(costate, -2); // push the Lua object onto the stack lua_remove(costate, -3); // remove the Lua object from the stack lua_pushstring(costate, keyName); // push the char onto the stack timeout = getTime() + 1.0; if (lua_pcall(costate, 2, 1, 0) != 0) { GetLogger()->error("Error while executing Lua Hook: {}\n", lua_tostring(costate, -1)); } else { handled = lua_toboolean(costate, -1) == 1 ? true : false; } lua_pop(costate, 1); // pop the return value } else { lua_pop(costate, 2); } return handled; } bool LuaState::callLuaHook(void* obj, const char* method, float x, float y) { if (!eventHandlerEnabled) return false; lua_pushlightuserdata(costate, obj); lua_gettable(costate, LUA_REGISTRYINDEX); if (!lua_istable(costate, -1)) { lua_pop(costate, 1); return false; } bool handled = false; lua_pushstring(costate, method); lua_gettable(costate, -2); if (lua_isfunction(costate, -1)) { lua_pushvalue(costate, -2); // push the Lua object onto the stack lua_remove(costate, -3); // remove the Lua object from the stack lua_pushnumber(costate, x); // push x onto the stack lua_pushnumber(costate, y); // push y onto the stack timeout = getTime() + 1.0; if (lua_pcall(costate, 3, 1, 0) != 0) { GetLogger()->error("Error while executing Lua Hook: {}\n", lua_tostring(costate, -1)); } else { handled = lua_toboolean(costate, -1) == 1 ? true : false; } lua_pop(costate, 1); // pop the return value } else { lua_pop(costate, 2); } return handled; } bool LuaState::callLuaHook(void* obj, const char* method, float x, float y, int b) { if (!eventHandlerEnabled) return false; lua_pushlightuserdata(costate, obj); lua_gettable(costate, LUA_REGISTRYINDEX); if (!lua_istable(costate, -1)) { lua_pop(costate, 1); return false; } bool handled = false; lua_pushstring(costate, method); lua_gettable(costate, -2); if (lua_isfunction(costate, -1)) { lua_pushvalue(costate, -2); // push the Lua object onto the stack lua_remove(costate, -3); // remove the Lua object from the stack lua_pushnumber(costate, x); // push x onto the stack lua_pushnumber(costate, y); // push y onto the stack lua_pushnumber(costate, b); // push b onto the stack timeout = getTime() + 1.0; if (lua_pcall(costate, 4, 1, 0) != 0) { GetLogger()->error("Error while executing Lua Hook: {}\n", lua_tostring(costate, -1)); } else { handled = lua_toboolean(costate, -1) == 1; } lua_pop(costate, 1); // pop the return value } else { lua_pop(costate, 2); } return handled; } bool LuaState::callLuaHook(void* obj, const char* method, double dt) { if (!eventHandlerEnabled) return false; lua_pushlightuserdata(costate, obj); lua_gettable(costate, LUA_REGISTRYINDEX); if (!lua_istable(costate, -1)) { lua_pop(costate, 1); return false; } bool handled = false; lua_pushstring(costate, method); lua_gettable(costate, -2); if (lua_isfunction(costate, -1)) { lua_pushvalue(costate, -2); // push the Lua object onto the stack lua_remove(costate, -3); // remove the Lua object from the stack lua_pushnumber(costate, dt); timeout = getTime() + 1.0; if (lua_pcall(costate, 2, 1, 0) != 0) { GetLogger()->error("Error while executing Lua Hook: {}\n", lua_tostring(costate, -1)); } else { handled = lua_toboolean(costate, -1) == 1; } lua_pop(costate, 1); // pop the return value } else { lua_pop(costate, 2); } return handled; } /**** Implementation of Celx LuaState wrapper ****/ bool CelxLua::isValid(int i) const { int argc = lua_gettop(m_lua); return i > 0 && i <= argc; } bool CelxLua::safeIsValid(int i, FatalErrors errors, const char *msg) { if (!isValid(i)) { if (errors & WrongArgc) doError(msg); return false; } return true; } CelxLua::CelxLua(lua_State* l) : m_lua(l) { } bool CelxLua::isType(int index, int type) const { return Celx_istype(m_lua, index, type); } Value *CelxLua::getValue(int index) { Value *v = nullptr; if (isInteger(index)) v = new Value((double)getInt(index)); else if (isNumber(index)) v = new Value(getNumber(index)); else if (isBoolean(index)) v = new Value(getBoolean(index)); else if (isString(index)) v = new Value(getString(index)); else if (isTable(index)) { ::Array *array = new ::Array; Hash *hash = new Hash; push(); while(lua_next(m_lua, index) != 0) { if (isInteger(-2)) { if (hash != nullptr) { delete hash; hash = nullptr; } if (array != nullptr) { array->push_back(getValue(-1)); } } else if (isString(-2)) { if (array != nullptr) { delete array; array = nullptr; } if (hash != nullptr) { hash->addValue(getString(-2), *getValue(-1)); } } pop(1); if (array == nullptr && hash == nullptr) break; } pop(1); if (hash != nullptr) v = new Value(hash); else if (array != nullptr) v = new Value(array); } return v; } void CelxLua::setClass(int id) { Celx_SetClass(m_lua, id); } // Push a class name onto the Lua stack void CelxLua::pushClassName(int id) { lua_pushlstring(m_lua, ClassNames[id], strlen(ClassNames[id])); } void* CelxLua::checkUserData(int index, int id) { return Celx_CheckUserData(m_lua, index, id); } void CelxLua::doError(const char* errorMessage) { Celx_DoError(m_lua, errorMessage); } void CelxLua::checkArgs(int minArgs, int maxArgs, const char* errorMessage) { Celx_CheckArgs(m_lua, minArgs, maxArgs, errorMessage); } void CelxLua::createClassMetatable(int id) { Celx_CreateClassMetatable(m_lua, id); } void CelxLua::registerMethod(const char* name, lua_CFunction fn) { Celx_RegisterMethod(m_lua, name, fn); } void CelxLua::registerValue(const char* name, float n) { lua_pushstring(m_lua, name); lua_pushnumber(m_lua, n); lua_settable(m_lua, -3); } // Add a field to the table on top of the stack void CelxLua::setTable(const char* field, lua_Number value) { lua_pushstring(m_lua, field); lua_pushnumber(m_lua, value); lua_settable(m_lua, -3); } void CelxLua::setTable(const char* field, const char* value) { lua_pushstring(m_lua, field); lua_pushstring(m_lua, value); lua_settable(m_lua, -3); } lua_Number CelxLua::safeGetNumber(int index, FatalErrors fatalErrors, const char* errorMessage, lua_Number defaultValue) { return Celx_SafeGetNumber(m_lua, index, fatalErrors, errorMessage, defaultValue); } const char* CelxLua::safeGetString(int index, FatalErrors fatalErrors, const char* errorMessage) { return Celx_SafeGetString(m_lua, index, fatalErrors, errorMessage); } const char *CelxLua::safeGetNonEmptyString(int index, FatalErrors fatalErrors, const char *errorMessage) { const char *s = safeGetString(index, fatalErrors, errorMessage); if (s == nullptr || *s == '\0') { doError(errorMessage); return nullptr; } return s; } bool CelxLua::safeGetBoolean(int index, FatalErrors fatalErrors, const char* errorMessage, bool defaultValue) { return Celx_SafeGetBoolean(m_lua, index, fatalErrors, errorMessage, defaultValue); } void CelxLua::newVector(const Vector3d& v) { vector_new(m_lua, v); } void CelxLua::newPosition(const UniversalCoord& uc) { position_new(m_lua, uc); } void CelxLua::newRotation(const Quaterniond& q) { rotation_new(m_lua, q); } void CelxLua::newObject(const Selection& sel) { object_new(m_lua, sel); } void CelxLua::newFrame(const ObserverFrame& f) { frame_new(m_lua, f); } void CelxLua::newPhase(const shared_ptr& phase) { phase_new(m_lua, phase); } Vector3d* CelxLua::toVector(int n) { return to_vector(m_lua, n); } Quaterniond* CelxLua::toRotation(int n) { return to_rotation(m_lua, n); } UniversalCoord* CelxLua::toPosition(int n) { return to_position(m_lua, n); } Selection* CelxLua::toObject(int n) { return to_object(m_lua, n); } ObserverFrame* CelxLua::toFrame(int n) { return to_frame(m_lua, n); } void CelxLua::push(const CelxValue& v1) { v1.push(m_lua); } void CelxLua::push(const CelxValue& v1, const CelxValue& v2) { v1.push(m_lua); v2.push(m_lua); } CelestiaCore* CelxLua::appCore(FatalErrors fatalErrors) { push("celestia-appcore"); lua_gettable(m_lua, LUA_REGISTRYINDEX); if (!lua_islightuserdata(m_lua, -1)) { if (fatalErrors == NoErrors) return nullptr; lua_pushstring(m_lua, "internal error: invalid appCore"); lua_error(m_lua); } CelestiaCore* appCore = static_cast(lua_touserdata(m_lua, -1)); lua_pop(m_lua, 1); return appCore; } // Get a pointer to the LuaState-object from the registry: LuaState* CelxLua::getLuaStateObject() { int stackSize = lua_gettop(m_lua); lua_pushstring(m_lua, "celestia-luastate"); lua_gettable(m_lua, LUA_REGISTRYINDEX); if (!lua_islightuserdata(m_lua, -1)) { Celx_DoError(m_lua, "Internal Error: Invalid table entry for LuaState-pointer"); return 0; } LuaState* luastate_ptr = static_cast(lua_touserdata(m_lua, -1)); if (luastate_ptr == nullptr) { Celx_DoError(m_lua, "Internal Error: Invalid LuaState-pointer"); return 0; } lua_settop(m_lua, stackSize); return luastate_ptr; }