Implementation of the new interface for Lua scripting

pull/3/head
Hleb Valoshka 2019-10-18 23:10:40 +03:00
parent d93a1cdf03
commit dfd177ffc7
8 changed files with 443 additions and 245 deletions

View File

@ -37,6 +37,7 @@
#include <celutil/debug.h>
#include <celutil/utf8.h>
#include <celcompat/filesystem.h>
#include <celcompat/memory.h>
#include <Eigen/Geometry>
#include <GL/glew.h>
#include <iostream>
@ -179,6 +180,7 @@ CelestiaCore::CelestiaCore() :
renderer(new Renderer()),
timer(new Timer()),
execEnv(new CoreExecutionEnvironment(*this)),
m_luaPlugin(make_unique<LuaScriptPlugin>(this)),
m_scriptMaps(make_shared<ScriptMaps>())
{
@ -200,13 +202,6 @@ CelestiaCore::~CelestiaCore()
if (movieCapture != nullptr)
recordEnd();
#ifdef CELX
// Clean up all scripts
delete celxScript;
delete luaHook;
delete luaSandbox;
#endif
delete execEnv;
delete timer;
delete renderer;
@ -326,12 +321,12 @@ void CelestiaCore::cancelScript()
runningScript = nullptr;
}
#ifdef CELX
if (celxScript != nullptr)
if (m_script != nullptr)
{
celxScript->cleanup();
if (textEnterMode & KbPassToScript)
setTextEnterMode(textEnterMode & ~KbPassToScript);
scriptState = ScriptCompleted;
m_script = nullptr;
}
#endif
}
@ -381,43 +376,11 @@ void CelestiaCore::runScript(const fs::path& filename)
}
}
#ifdef CELX
else if (type == Content_CelestiaScript)
else if (m_luaPlugin->isOurFile(localeFilename))
{
ifstream scriptfile(localeFilename.string());
if (!scriptfile.good())
{
string errMsg;
errMsg = fmt::sprintf(_("Error opening script '%s'"), localeFilename);
fatalError(errMsg);
}
if (celxScript == nullptr)
{
celxScript = new LuaState();
celxScript->init(this);
}
int status = celxScript->loadScript(scriptfile, localeFilename.string()); // FIXME
if (status != 0)
{
string errMsg = celxScript->getErrorMessage();
if (errMsg.empty())
errMsg = _("Unknown error opening script");
fatalError(errMsg);
}
else
{
// Coroutine execution; control may be transferred between the
// script and Celestia's event loop
if (!celxScript->createThread())
{
fatalError(_("Script coroutine initialization failed"));
}
else
{
scriptState = sim->getPauseState()?ScriptPaused:ScriptRunning;
}
}
m_script = m_luaPlugin->loadScript(localeFilename);
if (m_script != nullptr)
scriptState = sim->getPauseState() ? ScriptPaused : ScriptRunning;
}
#endif
else
@ -427,7 +390,7 @@ void CelestiaCore::runScript(const fs::path& filename)
}
bool checkMask(int modifiers, int mask)
static bool checkMask(int modifiers, int mask)
{
return (modifiers & mask) == mask;
}
@ -439,15 +402,14 @@ void CelestiaCore::mouseButtonDown(float x, float y, int button)
mouseMotion = 0.0f;
#ifdef CELX
if (celxScript != nullptr)
if (m_script != nullptr)
{
if (celxScript->handleMouseButtonEvent(x, y, button, true))
if (m_script->handleMouseButtonEvent(x, y, button, true))
return;
}
if (luaHook && luaHook->callLuaHook(this, "mousebuttondown", x, y, button))
return;
#endif
if (m_scriptHook != nullptr && m_scriptHook->call("mousebuttondown", x, y, button))
return;
if (views.size() > 1)
{
@ -515,15 +477,14 @@ void CelestiaCore::mouseButtonUp(float x, float y, int button)
}
#ifdef CELX
if (celxScript != nullptr)
if (m_script != nullptr)
{
if (celxScript->handleMouseButtonEvent(x, y, button, false))
if (m_script->handleMouseButtonEvent(x, y, button, false))
return;
}
if (luaHook && luaHook->callLuaHook(this,"mousebuttonup", x, y, button))
return;
#endif
if (m_scriptHook != nullptr && m_scriptHook->call("mousebuttonup", x, y, button))
return;
// If the mouse hasn't moved much since it was pressed, treat this
// as a selection or context menu event. Otherwise, assume that the
@ -611,10 +572,8 @@ void CelestiaCore::mouseWheel(float motion, int modifiers)
/// x and y are the pixel coordinates relative to the widget.
void CelestiaCore::mouseMove(float x, float y)
{
#ifdef CELX
if (luaHook && luaHook->callLuaHook(this, "mousemove", x, y))
if (m_scriptHook != nullptr && m_scriptHook->call("mousemove", x, y))
return;
#endif
if (views.size() > 1 && cursorHandler != nullptr)
{
@ -676,13 +635,9 @@ void CelestiaCore::mouseMove(float dx, float dy, int modifiers)
return;
}
#ifdef CELX
if (luaHook &&
luaHook->callLuaHook(this,"mousebuttonmove", dx, dy, modifiers))
{
return;
}
#endif
if (m_scriptHook != nullptr && m_scriptHook->call("mousebuttonmove", dx, dy, modifiers))
return;
if ((modifiers & (LeftButton | RightButton)) != 0)
{
@ -877,15 +832,9 @@ void CelestiaCore::keyDown(int key, int modifiers)
{
setViewChanged();
#ifdef CELX
// TODO: should pass modifiers as a Lua table
if (luaHook && luaHook->callLuaHook(this,
"keydown",
(float) key, (float) modifiers))
{
if (m_scriptHook != nullptr && m_scriptHook->call("keydown", float(key), float(modifiers)))
return;
}
#endif
switch (key)
{
case Key_F1:
@ -1020,9 +969,9 @@ void CelestiaCore::charEntered(const char *c_p, int modifiers)
#ifdef CELX
if (celxScript != nullptr && (textEnterMode & KbPassToScript))
if (m_script != nullptr && (textEnterMode & KbPassToScript))
{
if (c != '\033' && celxScript->charEntered(c_p))
if (c != '\033' && m_script->charEntered(c_p))
{
return;
}
@ -1125,25 +1074,18 @@ void CelestiaCore::charEntered(const char *c_p, int modifiers)
}
#ifdef CELX
if (celxScript != nullptr)
if (m_script != nullptr)
{
if (c != '\033')
{
string keyName = getKeyName(c_p, modifiers);
if (celxScript->handleKeyEvent(keyName.c_str()))
if (m_script->handleKeyEvent(keyName.c_str()))
return;
}
}
if (luaHook)
{
string keyName = getKeyName(c_p, modifiers);
if (luaHook->callLuaHook(this, "charentered", keyName.c_str()))
{
return;
}
}
#endif
if (m_scriptHook != nullptr && m_scriptHook->call("charentered", getKeyName(c_p, modifiers).c_str()))
return;
char C = toupper(c);
switch (C)
@ -1375,7 +1317,7 @@ void CelestiaCore::charEntered(const char *c_p, int modifiers)
// potentially confusing side effect of rendering nonfunctional
// goto, center, and other movement commands.
#ifdef CELX
if (runningScript != nullptr || celxScript != nullptr)
if (runningScript != nullptr || m_script != nullptr)
#else
if (runningScript != nullptr)
#endif
@ -2198,20 +2140,19 @@ void CelestiaCore::tick()
}
#ifdef CELX
if (celxScript != nullptr)
if (m_script != nullptr)
{
celxScript->handleTickEvent(dt);
m_script->handleTickEvent(dt);
if (scriptState == ScriptRunning)
{
bool finished = celxScript->tick(dt);
bool finished = m_script->tick(dt);
if (finished)
cancelScript();
}
}
if (luaHook != nullptr)
luaHook->callLuaHook(this, "tick", dt);
#endif // CELX
if (m_scriptHook != nullptr)
m_scriptHook->call("tick", dt);
sim->update(dt);
}
@ -2305,10 +2246,8 @@ void CelestiaCore::resize(GLsizei w, GLsizei h)
height = h;
setFOVFromZoom();
#ifdef CELX
if (luaHook && luaHook->callLuaHook(this,"resize", (float) w, (float) h))
if (m_scriptHook != nullptr && m_scriptHook->call("resize", float(w), float(h)))
return;
#endif
}
@ -3076,9 +3015,9 @@ void CelestiaCore::setScriptImage(std::unique_ptr<OverlayImage> &&_image)
void CelestiaCore::renderOverlay()
{
#ifdef CELX
if (luaHook) luaHook->callLuaHook(this, "renderoverlay");
#endif
if (m_scriptHook != nullptr)
m_scriptHook->call("renderoverlay");
if (font == nullptr)
return;
@ -3091,7 +3030,7 @@ void CelestiaCore::renderOverlay()
overlay->begin();
#ifdef CELX
if (runningScript || celxScript)
if (runningScript || m_script != nullptr)
{
#else
if (runningScript)
@ -4553,146 +4492,9 @@ bool CelestiaCore::referenceMarkEnabled(const string& refMark, Selection sel) co
#ifdef CELX
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")
{
if (dirs.count(dir) == 0)
dirs.insert(dir);
}
};
};
// 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 CelestiaCore::initLuaHook(ProgressNotifier* progressNotifier)
{
luaHook = new LuaState();
luaHook->init(this);
string LuaPath = "?.lua;celxx/?.lua;";
// Find the path for lua files in the extras directories
for (const auto& dir : config->extrasDirs)
{
if (!is_valid_directory(dir))
continue;
LuaPathFinder loader;
for (const auto& fn : fs::recursive_directory_iterator(dir))
loader.process(fn);
LuaPath += loader.getLuaPath();
}
// 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())
{
string errMsg;
errMsg = fmt::sprintf(_("Error opening LuaHook '%s'"), config->luaHook);
fatalError(errMsg);
}
if (progressNotifier)
progressNotifier->update(config->luaHook.string());
status = luaHook->loadScript(scriptfile, config->luaHook);
}
else
{
status = luaHook->loadScript("");
}
if (status != 0)
{
cerr << "lua hook load failed\n";
string errMsg = luaHook->getErrorMessage();
if (errMsg.empty())
errMsg = _("Unknown error loading hook script");
fatalError(errMsg);
delete luaHook;
luaHook = nullptr;
}
else
{
// Coroutine execution; control may be transferred between the
// script and Celestia's event loop
if (!luaHook->createThread())
{
cerr << "hook thread failed\n";
string errMsg = _("Script coroutine initialization failed");
fatalError(errMsg);
delete luaHook;
luaHook = nullptr;
}
if (luaHook)
{
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)
{
SetScriptedObjectContext(luaHook->getState());
}
}
else
{
luaSandbox = new LuaState();
luaSandbox->init(this);
// 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)
{
delete luaSandbox;
luaSandbox = nullptr;
return false;
}
SetScriptedObjectContext(luaSandbox->getState());
}
return true;
return CreateLuaEnvironment(this, config, progressNotifier);
}
#endif

View File

@ -30,7 +30,9 @@
#include "view.h"
#ifdef CELX
#include <celscript/lua/celx.h>
#include <celscript/lua/luascript.h>
#endif
#include <celscript/common/script.h>
#include <celscript/common/scriptmaps.h>
class Url;
@ -334,6 +336,7 @@ class CelestiaCore // : public Watchable<CelestiaCore>
const std::string& getTypedText() const { return typedText; }
void setTypedText(const char *);
void setScriptHook(std::unique_ptr<celestia::scripts::IScriptHook> &&hook) { m_scriptHook = std::move(hook); }
const std::shared_ptr<celestia::scripts::ScriptMaps>& scriptMaps() const { return m_scriptMaps; }
protected:
@ -395,12 +398,10 @@ class CelestiaCore // : public Watchable<CelestiaCore>
Execution* runningScript{ nullptr };
ExecutionEnvironment* execEnv{ nullptr };
#ifdef CELX
LuaState* celxScript{ nullptr };
LuaState* luaHook{ nullptr }; // Lua hook context
LuaState* luaSandbox{ nullptr }; // Safe Lua context for ssc scripts
#endif // CELX
std::shared_ptr<celestia::scripts::ScriptMaps> m_scriptMaps;
std::unique_ptr<celestia::scripts::IScript> m_script;
std::unique_ptr<celestia::scripts::IScriptHook> m_scriptHook;
std::unique_ptr<celestia::scripts::LuaScriptPlugin> m_luaPlugin;
std::shared_ptr<celestia::scripts::ScriptMaps> m_scriptMaps;
enum ScriptState
{

View File

@ -24,6 +24,8 @@ set(CELX_SOURCES
celx_rotation.h
celx_vector.cpp
celx_vector.h
luascript.cpp
luascript.h
)
add_library(celluascript OBJECT ${CELX_SOURCES})

View File

@ -1,3 +1,12 @@
// celx_category.h
//
// 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.
#pragma once
#include "celx_internal.h"

View File

@ -1,3 +1,11 @@
// celx_misc.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 <celutil/debug.h>
#include "celx_misc.h"

View File

@ -1,3 +1,12 @@
// celx_misc.h
//
// 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.
#pragma once
#include "celx_internal.h"

View File

@ -0,0 +1,280 @@
// 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/printf.h>
#include <celcompat/filesystem.h>
#include <celcompat/memory.h>
#include <celephem/scriptobject.h>
#include <celestia/configfile.h>
#include <celestia/celestiacore.h>
#include <celutil/util.h>
#include "celx_internal.h"
#include "luascript.h"
using namespace std;
namespace celestia
{
namespace scripts
{
LuaScript::LuaScript(CelestiaCore *appcore) :
m_appCore(appcore),
m_celxScript(make_unique<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.string()) != 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 = make_unique<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;
if (!is_directory(dir))
{
fmt::fprintf(cerr, "Path %s doesn't exist or isn't a directory", dir);
continue;
}
LuaPathFinder loader;
for (const auto& fn : fs::recursive_directory_iterator(dir))
loader.process(fn);
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)
{
cerr << "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())
{
cerr << "hook thread failed\n";
appCore->fatalError(_("Script coroutine initialization failed"));
luaHook = nullptr;
}
if (luaHook != nullptr)
{
auto lh = make_unique<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;
}
}
}

View File

@ -0,0 +1,87 @@
// luascript.h
//
// 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.
#pragma once
#include <celscript/common/script.h>
#include <iosfwd>
class LuaState;
class CelestiaConfig;
class CelestiaCore;
class ProgressNotifier;
namespace celestia
{
namespace scripts
{
class LuaScript : public IScript
{
public:
LuaScript(CelestiaCore*);
~LuaScript() override;
bool load(std::ifstream&, const fs::path&, std::string&);
bool handleMouseButtonEvent(float x, float y, int button, bool down) override;
bool charEntered(const char*) override;
bool handleKeyEvent(const char* key) override;
bool handleTickEvent(double dt) override;
bool tick(double) override;
private:
CelestiaCore *m_appCore;
std::unique_ptr<LuaState> m_celxScript;
friend class LuaScriptPlugin;
};
class LuaScriptPlugin : public IScriptPlugin
{
public:
LuaScriptPlugin() = delete;
LuaScriptPlugin(CelestiaCore *appCore) : IScriptPlugin(appCore) {};
~LuaScriptPlugin() override = default;
LuaScriptPlugin(const LuaScriptPlugin&) = delete;
LuaScriptPlugin(LuaScriptPlugin&&) = delete;
LuaScriptPlugin& operator=(const LuaScriptPlugin&) = delete;
LuaScriptPlugin& operator=(LuaScriptPlugin&&) = delete;
bool isOurFile(const fs::path&) const override;
std::unique_ptr<IScript> loadScript(const fs::path&) override;
};
class LuaHook : public IScriptHook
{
public:
LuaHook() = delete;
LuaHook(CelestiaCore *appCore) : IScriptHook(appCore) {};
~LuaHook() override = default;
LuaHook(const LuaHook&) = default;
LuaHook(LuaHook&&) = default;
LuaHook& operator=(const LuaHook&) = default;
LuaHook& operator=(LuaHook&&) = default;
bool call(const char *method) const override;
bool call(const char *method, const char *keyName) const override;
bool call(const char *method, float x, float y) const override;
bool call(const char *method, float x, float y, int b) const override;
bool call(const char *method, double dt) const override;
private:
std::unique_ptr<LuaState> m_state;
friend bool CreateLuaEnvironment(CelestiaCore*, const CelestiaConfig*, ProgressNotifier*);
};
bool CreateLuaEnvironment(CelestiaCore *appCore, const CelestiaConfig *config, ProgressNotifier *progressNotifier = nullptr);
}
}