celestia/src/celscript/lua/luascript.cpp

289 lines
7.5 KiB
C++

// luascript.cpp
//
// Copyright (C) 2019, the Celestia Development Team
//
// 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 <set>
#include <string>
#include <fstream>
#include <fmt/ostream.h>
#include <celcompat/filesystem.h>
#include <celephem/scriptobject.h>
#include <celestia/configfile.h>
#include <celestia/celestiacore.h>
#include <celutil/gettext.h>
#include <celutil/logger.h>
#include "celx_internal.h"
#include "luascript.h"
using namespace std;
namespace celestia
{
using util::GetLogger;
namespace scripts
{
LuaScript::LuaScript(CelestiaCore *appcore) :
m_appCore(appcore),
m_celxScript(new LuaState)
{
m_celxScript->init(m_appCore);
}
LuaScript::~LuaScript()
{
m_celxScript->cleanup();
}
bool LuaScript::load(ifstream &scriptfile, const fs::path &path, string &errorMsg)
{
if (m_celxScript->loadScript(scriptfile, path) != 0)
{
errorMsg = m_celxScript->getErrorMessage();
return false;
}
return true;
}
bool LuaScript::handleMouseButtonEvent(float x, float y, int button, bool down)
{
return m_celxScript->handleMouseButtonEvent(x, y, button, down);
}
bool LuaScript::charEntered(const char* c)
{
return m_celxScript->charEntered(c);
}
bool LuaScript::handleKeyEvent(const char* key)
{
return m_celxScript->handleKeyEvent(key);
}
bool LuaScript::handleTickEvent(double dt)
{
return m_celxScript->handleTickEvent(dt);
}
bool LuaScript::tick(double dt)
{
return m_celxScript->tick(dt);
}
bool LuaScriptPlugin::isOurFile(const fs::path &p) const
{
auto ext = p.extension();
return ext == ".celx" || ext == ".clx";
}
unique_ptr<IScript> LuaScriptPlugin::loadScript(const fs::path &path)
{
ifstream scriptfile(path.string());
if (!scriptfile.good())
{
appCore()->fatalError(fmt::sprintf(_("Error opening script '%s'"), path));
return nullptr;
}
auto script = unique_ptr<LuaScript>(new LuaScript(appCore()));
string errMsg;
if (!script->load(scriptfile, path, errMsg))
{
if (errMsg.empty())
errMsg = _("Unknown error loading script");
appCore()->fatalError(errMsg);
return nullptr;
}
// Coroutine execution; control may be transferred between the
// script and Celestia's event loop
if (!script->m_celxScript->createThread())
{
appCore()->fatalError(_("Script coroutine initialization failed"));
return nullptr;
}
return script;
}
bool LuaHook::call(const char *method) const
{
return m_state->callLuaHook(appCore(), method);
}
bool LuaHook::call(const char *method, const char *keyName) const
{
return m_state->callLuaHook(appCore(), method, keyName);
}
bool LuaHook::call(const char *method, float x, float y) const
{
return m_state->callLuaHook(appCore(), method, x, y);
}
bool LuaHook::call(const char *method, float x, float y, int b) const
{
return m_state->callLuaHook(appCore(), method, x, y, b);
}
bool LuaHook::call(const char *method, double dt) const
{
return m_state->callLuaHook(appCore(), method, dt);
}
class LuaPathFinder
{
set<fs::path> dirs;
public:
const string getLuaPath() const
{
string out;
for (const auto& dir : dirs)
out += (dir / "?.lua;").string();
return out;
}
void process(const fs::path& p)
{
auto dir = p.parent_path();
if (p.extension() == ".lua" && dirs.count(dir) == 0)
dirs.insert(dir);
}
};
static string lua_path(const CelestiaConfig *config)
{
string LuaPath = "?.lua;celxx/?.lua;";
// Find the path for lua files in the extras directories
for (const auto& dir : config->extrasDirs)
{
if (dir.empty())
continue;
std::error_code ec;
if (!fs::is_directory(dir, ec))
{
GetLogger()->warn(_("Path {} doesn't exist or isn't a directory\n"), dir);
continue;
}
LuaPathFinder loader;
auto iter = fs::recursive_directory_iterator(dir, ec);
for (; iter != end(iter); iter.increment(ec))
{
if (ec)
continue;
loader.process(*iter);
}
LuaPath += loader.getLuaPath();
}
return LuaPath;
}
// Initialize the Lua hook table as well as the Lua state for scripted
// objects. The Lua hook operates in a different Lua state than user loaded
// scripts. It always has file system access via the IO package. If the script
// system access policy is "allow", then scripted objects will run in the same
// Lua context as the Lua hook. Sharing state between scripted objects and the
// hook can be very useful, but it gives system access to scripted objects,
// and therefore must be restricted based on the system access policy.
bool CreateLuaEnvironment(CelestiaCore *appCore, const CelestiaConfig *config, ProgressNotifier *progressNotifier)
{
auto LuaPath = lua_path(config);
LuaState *luaHook = new LuaState();
luaHook->init(appCore);
// Always grant access for the Lua hook
luaHook->allowSystemAccess();
luaHook->setLuaPath(LuaPath);
int status = 0;
// Execute the Lua hook initialization script
if (!config->luaHook.empty())
{
ifstream scriptfile(config->luaHook.string());
if (!scriptfile.good())
appCore->fatalError(fmt::sprintf(_("Error opening LuaHook '%s'"), config->luaHook));
if (progressNotifier != nullptr)
progressNotifier->update(config->luaHook.string());
status = luaHook->loadScript(scriptfile, config->luaHook);
}
else
{
status = luaHook->loadScript("");
}
if (status != 0)
{
GetLogger()->debug("lua hook load failed\n");
string errMsg = luaHook->getErrorMessage();
if (errMsg.empty())
errMsg = _("Unknown error loading hook script");
appCore->fatalError(errMsg);
luaHook = nullptr;
}
else
{
// Coroutine execution; control may be transferred between the
// script and Celestia's event loop
if (!luaHook->createThread())
{
GetLogger()->debug("hook thread failed\n");
appCore->fatalError(_("Script coroutine initialization failed"));
luaHook = nullptr;
}
if (luaHook != nullptr)
{
auto lh = unique_ptr<LuaHook>(new LuaHook(appCore));
lh->m_state = unique_ptr<LuaState>(luaHook);
appCore->setScriptHook(std::move(lh));
while (!luaHook->tick(0.1)) ;
}
}
// Set up the script context; if the system access policy is allow,
// it will share the same context as the Lua hook. Otherwise, we
// create a private context.
if (config->scriptSystemAccessPolicy == "allow")
{
if (luaHook != nullptr)
SetScriptedObjectContext(luaHook->getState());
}
else
{
auto luaSandbox = new LuaState();
luaSandbox->init(appCore);
// Allow access to functions in package because we need 'require'
// But, loadlib is prohibited.
luaSandbox->allowLuaPackageAccess();
luaSandbox->setLuaPath(LuaPath);
status = luaSandbox->loadScript("");
if (status != 0)
{
luaSandbox = nullptr;
return false;
}
SetScriptedObjectContext(luaSandbox->getState());
}
return true;
}
}
}