Files
OpenRCT2-Unity/src/openrct2/scripting/ScriptEngine.cpp

704 lines
19 KiB
C++
Raw Normal View History

/*****************************************************************************
* Copyright (c) 2014-2018 OpenRCT2 developers
*
* For a complete list of all authors, please refer to contributors.md
* Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
*
* OpenRCT2 is licensed under the GNU General Public License version 3.
*****************************************************************************/
2020-02-23 12:55:48 +00:00
#ifdef __ENABLE_SCRIPTING__
2020-02-23 12:55:48 +00:00
# include "ScriptEngine.h"
2020-02-23 12:55:48 +00:00
# include "../PlatformEnvironment.h"
# include "../config/Config.h"
# include "../core/FileScanner.h"
# include "../core/Path.hpp"
# include "../interface/InteractiveConsole.h"
# include "../platform/Platform2.h"
# include "Duktape.hpp"
# include "ScConsole.hpp"
# include "ScContext.hpp"
# include "ScDate.hpp"
# include "ScDisposable.hpp"
# include "ScMap.hpp"
# include "ScNetwork.hpp"
# include "ScPark.hpp"
# include "ScRide.hpp"
# include "ScThing.hpp"
# include "ScTile.hpp"
# include <iostream>
# include <stdexcept>
2018-03-17 23:26:55 +00:00
using namespace OpenRCT2;
using namespace OpenRCT2::Scripting;
2020-02-06 23:15:20 +00:00
static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 1;
2020-02-25 00:43:49 +00:00
struct ExpressionStringifier final
{
private:
std::stringstream _ss;
duk_context* _context{};
int32_t _indent{};
ExpressionStringifier(duk_context* ctx)
: _context(ctx)
{
}
void PushIndent(int32_t c = 1)
{
_indent += c;
}
void PopIndent(int32_t c = 1)
{
_indent -= c;
}
void LineFeed()
{
_ss << "\n" << std::string(_indent, ' ');
}
void Stringify(const DukValue& val, bool canStartWithNewLine)
{
switch (val.type())
{
case DukValue::Type::UNDEFINED:
_ss << "undefined";
break;
case DukValue::Type::NULLREF:
_ss << "null";
break;
case DukValue::Type::BOOLEAN:
StringifyBoolean(val);
break;
case DukValue::Type::NUMBER:
StringifyNumber(val);
break;
case DukValue::Type::STRING:
_ss << "'" << val.as_string() << "'";
break;
case DukValue::Type::OBJECT:
if (val.is_function())
{
StringifyFunction(val);
}
else if (val.is_array())
{
StringifyArray(val, canStartWithNewLine);
}
else
{
StringifyObject(val, canStartWithNewLine);
}
break;
case DukValue::Type::BUFFER:
_ss << "[Buffer]";
break;
case DukValue::Type::POINTER:
_ss << "[Pointer]";
break;
case DukValue::Type::LIGHTFUNC:
_ss << "[LightFunc]";
break;
}
}
void StringifyArray(const DukValue& val, bool canStartWithNewLine)
{
constexpr auto maxItemsToShow = 4;
val.push();
auto arrayLen = duk_get_length(_context, -1);
if (arrayLen == 0)
{
_ss << "[]";
}
else if (arrayLen == 1)
{
_ss << "[ ";
for (duk_uarridx_t i = 0; i < arrayLen; i++)
{
if (duk_get_prop_index(_context, -1, i))
{
if (i != 0)
{
_ss << ", ";
}
Stringify(DukValue::take_from_stack(_context), false);
}
}
_ss << " ]";
}
else
{
if (canStartWithNewLine)
{
PushIndent();
LineFeed();
}
_ss << "[ ";
PushIndent(2);
for (duk_uarridx_t i = 0; i < arrayLen; i++)
{
if (i != 0)
{
_ss << ",";
LineFeed();
}
if (i >= maxItemsToShow)
{
auto remainingItemsNotShown = arrayLen - maxItemsToShow;
if (remainingItemsNotShown == 1)
{
_ss << "... 1 more item";
}
else
{
_ss << "... " << std::to_string(remainingItemsNotShown) << " more items";
}
break;
}
else
{
if (duk_get_prop_index(_context, -1, i))
{
Stringify(DukValue::take_from_stack(_context), false);
}
}
}
_ss << " ]";
PopIndent(2);
if (canStartWithNewLine)
{
PopIndent();
}
}
duk_pop(_context);
}
void StringifyObject(const DukValue& val, bool canStartWithNewLine)
{
auto numEnumerables = GetNumEnumerablesOnObject(val);
if (numEnumerables == 0)
{
_ss << "{}";
}
else if (numEnumerables == 1)
{
_ss << "{ ";
val.push();
duk_enum(_context, -1, 0);
auto index = 0;
while (duk_next(_context, -1, 1))
{
if (index != 0)
{
_ss << ", ";
}
auto value = DukValue::take_from_stack(_context, -1);
auto key = DukValue::take_from_stack(_context, -1);
if (key.type() == DukValue::Type::STRING)
{
_ss << key.as_string() << ": ";
}
else
{
// For some reason the key was not a string
_ss << "?: ";
}
Stringify(value, true);
index++;
}
duk_pop_2(_context);
_ss << " }";
}
else
{
if (canStartWithNewLine)
{
PushIndent();
LineFeed();
}
_ss << "{ ";
PushIndent(2);
val.push();
duk_enum(_context, -1, 0);
auto index = 0;
while (duk_next(_context, -1, 1))
{
if (index != 0)
{
_ss << ",";
LineFeed();
}
auto value = DukValue::take_from_stack(_context, -1);
auto key = DukValue::take_from_stack(_context, -1);
if (key.type() == DukValue::Type::STRING)
{
_ss << key.as_string() << ": ";
}
else
{
// For some reason the key was not a string
_ss << "?: ";
}
Stringify(value, true);
index++;
}
duk_pop_2(_context);
PopIndent(2);
_ss << " }";
if (canStartWithNewLine)
{
PopIndent();
}
}
}
void StringifyFunction(const DukValue& val)
{
val.push();
if (duk_is_c_function(_context, -1))
{
_ss << "[Native Function]";
}
else if (duk_is_ecmascript_function(_context, -1))
{
_ss << "[ECMAScript Function]";
}
else
{
_ss << "[Function]";
}
duk_pop(_context);
}
void StringifyBoolean(const DukValue& val)
{
2020-02-25 21:10:28 +00:00
_ss << (val.as_bool() ? "true" : "false");
2020-02-25 00:43:49 +00:00
}
void StringifyNumber(const DukValue& val)
{
const auto d = val.as_double();
const duk_int_t i = val.as_int();
if (AlmostEqual<double>(d, i))
{
_ss << std::to_string(i);
}
else
{
_ss << std::to_string(d);
}
}
size_t GetNumEnumerablesOnObject(const DukValue& val)
{
size_t count = 0;
val.push();
duk_enum(_context, -1, 0);
while (duk_next(_context, -1, 0))
{
count++;
duk_pop(_context);
}
duk_pop_2(_context);
return count;
}
// Taken from http://en.cppreference.com/w/cpp/types/numeric_limits/epsilon
template<class T>
static typename std::enable_if<!std::numeric_limits<T>::is_integer, bool>::type AlmostEqual(T x, T y, int32_t ulp = 20)
{
// the machine epsilon has to be scaled to the magnitude of the values used
// and multiplied by the desired precision in ULPs (units in the last place)
return std::abs(x - y) <= std::numeric_limits<T>::epsilon() * std::abs(x + y) * ulp
// unless the result is subnormal
|| std::abs(x - y)
< (std::numeric_limits<T>::min)(); // TODO: Remove parentheses around min once the macro is removed
}
public:
static std::string StringifyExpression(const DukValue& val)
{
ExpressionStringifier instance(val.context());
instance.Stringify(val, false);
return instance._ss.str();
}
};
DukContext::DukContext()
{
_context = duk_create_heap_default();
if (_context == nullptr)
{
throw std::runtime_error("Unable to initialise duktape context.");
}
}
DukContext::~DukContext()
{
duk_destroy_heap(_context);
}
ScriptEngine::ScriptEngine(InteractiveConsole& console, IPlatformEnvironment& env)
: _console(console)
, _env(env)
, _hookEngine(_execInfo)
{
}
2018-03-17 23:26:55 +00:00
void ScriptEngine::Initialise()
{
auto ctx = (duk_context*)_context;
2018-03-17 23:26:55 +00:00
ScConsole::Register(ctx);
2018-03-18 23:35:58 +00:00
ScContext::Register(ctx);
2020-02-18 22:31:54 +00:00
ScDate::Register(ctx);
2018-03-18 23:35:58 +00:00
ScDisposable::Register(ctx);
2018-03-20 19:40:38 +00:00
ScMap::Register(ctx);
2019-07-24 21:08:03 +01:00
ScNetwork::Register(ctx);
2018-03-18 00:31:02 +00:00
ScPark::Register(ctx);
2019-07-24 21:08:03 +01:00
ScPlayer::Register(ctx);
2019-07-25 20:45:38 +01:00
ScPlayerGroup::Register(ctx);
2020-02-12 17:32:24 +00:00
ScRide::Register(ctx);
ScRideObject::Register(ctx);
2018-03-20 20:28:15 +00:00
ScTile::Register(ctx);
ScTileElement::Register(ctx);
2018-03-20 19:40:38 +00:00
ScThing::Register(ctx);
2018-03-17 23:26:55 +00:00
2018-03-18 00:31:02 +00:00
dukglue_register_global(ctx, std::make_shared<ScConsole>(_console), "console");
2018-03-18 23:35:58 +00:00
dukglue_register_global(ctx, std::make_shared<ScContext>(_execInfo, _hookEngine), "context");
2020-02-18 22:31:54 +00:00
dukglue_register_global(ctx, std::make_shared<ScDate>(), "date");
2018-03-20 19:40:38 +00:00
dukglue_register_global(ctx, std::make_shared<ScMap>(ctx), "map");
2019-07-24 21:08:03 +01:00
dukglue_register_global(ctx, std::make_shared<ScNetwork>(ctx), "network");
2018-03-18 00:31:02 +00:00
dukglue_register_global(ctx, std::make_shared<ScPark>(), "park");
2018-03-18 16:27:48 +00:00
2020-02-06 23:15:20 +00:00
_initialised = true;
_pluginsLoaded = false;
_pluginsStarted = false;
2018-03-18 16:27:48 +00:00
}
void ScriptEngine::LoadPlugins()
{
if (!_initialised)
2018-03-18 16:27:48 +00:00
{
Initialise();
2018-03-18 16:27:48 +00:00
}
if (_pluginsLoaded)
{
UnloadPlugins();
}
2018-03-31 00:08:59 +01:00
auto base = _env.GetDirectoryPath(DIRBASE::USER, DIRID::PLUGIN);
if (Path::DirectoryExists(base))
{
auto pattern = Path::Combine(base, "*.js");
auto scanner = std::unique_ptr<IFileScanner>(Path::ScanDirectory(pattern, true));
while (scanner->Next())
{
auto path = std::string(scanner->GetPath());
if (ShouldLoadScript(path))
{
LoadPlugin(path);
}
}
if (gConfigPlugin.enable_hot_reloading && network_get_mode() == NETWORK_MODE_NONE)
{
SetupHotReloading();
}
}
_pluginsLoaded = true;
_pluginsStarted = false;
}
void ScriptEngine::LoadPlugin(const std::string& path)
{
auto plugin = std::make_shared<Plugin>(_context, path);
LoadPlugin(plugin);
}
void ScriptEngine::LoadPlugin(std::shared_ptr<Plugin>& plugin)
{
try
{
2020-02-23 02:00:22 +00:00
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
plugin->Load();
auto metadata = plugin->GetMetadata();
if (metadata.MinApiVersion <= OPENRCT2_PLUGIN_API_VERSION)
{
LogPluginInfo(plugin, "Loaded");
_plugins.push_back(std::move(plugin));
}
else
{
LogPluginInfo(plugin, "Requires newer API version: v" + std::to_string(metadata.MinApiVersion));
}
}
catch (const std::exception& e)
{
_console.WriteLineError(e.what());
}
2018-03-18 16:27:48 +00:00
}
2020-02-11 21:26:05 +00:00
void ScriptEngine::StopPlugin(std::shared_ptr<Plugin> plugin)
{
if (plugin->HasStarted())
{
_hookEngine.UnsubscribeAll(plugin);
for (auto callback : _pluginStoppedSubscriptions)
{
callback(plugin);
}
2020-02-23 02:00:22 +00:00
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
2020-02-11 21:26:05 +00:00
try
{
plugin->Stop();
}
catch (const std::exception& e)
{
_console.WriteLineError(e.what());
}
}
}
2020-02-06 21:55:53 +00:00
bool ScriptEngine::ShouldLoadScript(const std::string& path)
{
// A lot of JavaScript is often found in a node_modules directory tree and is most likely unwanted, so ignore it
return path.find("/node_modules/") == std::string::npos && path.find("\\node_modules\\") == std::string::npos;
}
void ScriptEngine::SetupHotReloading()
{
try
{
auto base = _env.GetDirectoryPath(DIRBASE::USER, DIRID::PLUGIN);
_pluginFileWatcher = std::make_unique<FileWatcher>(base);
_pluginFileWatcher->OnFileChanged = [this](const std::string& path) {
std::lock_guard<std::mutex> guard(_changedPluginFilesMutex);
_changedPluginFiles.emplace(path);
};
}
catch (const std::exception& e)
{
std::printf("Unable to enable hot reloading of plugins: %s\n", e.what());
}
}
2018-03-18 17:43:47 +00:00
void ScriptEngine::AutoReloadPlugins()
{
2018-03-31 00:08:59 +01:00
if (_changedPluginFiles.size() > 0)
2018-03-18 17:43:47 +00:00
{
2018-03-31 00:08:59 +01:00
std::lock_guard<std::mutex> guard(_changedPluginFilesMutex);
for (auto& path : _changedPluginFiles)
2018-03-18 17:43:47 +00:00
{
auto findResult = std::find_if(_plugins.begin(), _plugins.end(), [&path](const std::shared_ptr<Plugin>& plugin) {
return Path::Equals(path, plugin->GetPath());
});
2018-03-31 00:08:59 +01:00
if (findResult != _plugins.end())
2018-03-18 17:43:47 +00:00
{
2018-03-31 00:08:59 +01:00
auto& plugin = *findResult;
try
{
2020-02-11 21:26:05 +00:00
StopPlugin(plugin);
2018-03-23 23:24:36 +00:00
2020-02-23 02:00:22 +00:00
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
2018-03-31 00:08:59 +01:00
plugin->Load();
2020-02-06 23:15:20 +00:00
LogPluginInfo(plugin, "Reloaded");
2018-03-31 00:08:59 +01:00
plugin->Start();
}
catch (const std::exception& e)
2018-03-31 00:08:59 +01:00
{
_console.WriteLineError(e.what());
}
2018-03-18 17:43:47 +00:00
}
}
2018-03-31 00:08:59 +01:00
_changedPluginFiles.clear();
2018-03-18 17:43:47 +00:00
}
}
2020-02-06 23:15:20 +00:00
void ScriptEngine::UnloadPlugins()
{
StopPlugins();
for (auto& plugin : _plugins)
{
LogPluginInfo(plugin, "Unloaded");
}
_plugins.clear();
_pluginsLoaded = false;
_pluginsStarted = false;
}
2018-03-18 16:27:48 +00:00
void ScriptEngine::StartPlugins()
{
for (auto& plugin : _plugins)
{
if (!plugin->HasStarted() && ShouldStartPlugin(plugin))
2018-03-18 23:35:58 +00:00
{
2020-02-23 02:00:22 +00:00
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
2020-02-06 23:15:20 +00:00
try
{
LogPluginInfo(plugin, "Started");
2020-02-06 23:15:20 +00:00
plugin->Start();
}
catch (const std::exception& e)
{
_console.WriteLineError(e.what());
}
2018-03-18 23:35:58 +00:00
}
2018-03-18 16:27:48 +00:00
}
2020-02-06 23:15:20 +00:00
_pluginsStarted = true;
}
bool ScriptEngine::ShouldStartPlugin(const std::shared_ptr<Plugin>& plugin)
{
auto networkMode = network_get_mode();
if (networkMode == NETWORK_MODE_CLIENT)
{
// Only client plugins and plugins downloaded from server should be started
const auto& metadata = plugin->GetMetadata();
if (metadata.Type == PluginType::Remote && plugin->HasPath())
{
LogPluginInfo(plugin, "Remote plugin not started");
return false;
}
}
return true;
}
2020-02-06 23:15:20 +00:00
void ScriptEngine::StopPlugins()
{
for (auto& plugin : _plugins)
{
if (plugin->HasStarted())
{
StopPlugin(plugin);
LogPluginInfo(plugin, "Stopped");
}
2020-02-06 23:15:20 +00:00
}
_pluginsStarted = false;
2018-03-17 23:26:55 +00:00
}
void ScriptEngine::Update()
{
2018-03-17 23:26:55 +00:00
if (!_initialised)
{
Initialise();
}
2020-02-06 23:15:20 +00:00
if (_pluginsLoaded)
{
if (!_pluginsStarted)
{
StartPlugins();
}
else
{
auto tick = Platform::GetTicks();
if (tick - _lastHotReloadCheckTick > 1000)
{
AutoReloadPlugins();
_lastHotReloadCheckTick = tick;
}
}
}
ProcessREPL();
}
void ScriptEngine::ProcessREPL()
{
while (_evalQueue.size() > 0)
{
auto item = std::move(_evalQueue.front());
_evalQueue.pop();
auto promise = std::move(std::get<0>(item));
auto command = std::move(std::get<1>(item));
if (duk_peval_string(_context, command.c_str()) != 0)
{
std::string result = std::string(duk_safe_to_string(_context, -1));
_console.WriteLineError(result);
}
2019-07-24 21:08:03 +01:00
else if (duk_get_type(_context, -1) != DUK_TYPE_UNDEFINED)
{
2020-02-24 19:25:18 +00:00
auto result = Stringify(DukValue::copy_from_stack(_context, -1));
_console.WriteLine(result);
}
duk_pop(_context);
// Signal the promise so caller can continue
promise.set_value();
}
}
std::future<void> ScriptEngine::Eval(const std::string& s)
{
std::promise<void> barrier;
auto future = barrier.get_future();
_evalQueue.emplace(std::move(barrier), s);
return future;
}
2020-02-06 23:15:20 +00:00
void ScriptEngine::LogPluginInfo(const std::shared_ptr<Plugin>& plugin, const std::string_view& message)
{
const auto& pluginName = plugin->GetMetadata().Name;
_console.WriteLine("[" + pluginName + "] " + std::string(message));
}
void ScriptEngine::AddNetworkPlugin(const std::string_view& code)
{
auto plugin = std::make_shared<Plugin>(_context, std::string());
plugin->SetCode(code);
LoadPlugin(plugin);
}
2020-02-25 00:43:49 +00:00
std::string OpenRCT2::Scripting::Stringify(const DukValue& val)
{
2020-02-25 00:43:49 +00:00
return ExpressionStringifier::StringifyExpression(val);
}
2020-02-23 02:00:22 +00:00
bool OpenRCT2::Scripting::IsGameStateMutable()
{
// Allow single player to alter game state anywhere
if (network_get_mode() == NETWORK_MODE_NONE)
{
return true;
}
else
{
auto& scriptEngine = GetContext()->GetScriptEngine();
auto& execInfo = scriptEngine.GetExecInfo();
return execInfo.IsGameStateMutable();
}
2020-02-23 02:00:22 +00:00
}
void OpenRCT2::Scripting::ThrowIfGameStateNotMutable()
{
// Allow single player to alter game state anywhere
if (network_get_mode() != NETWORK_MODE_NONE)
2020-02-23 02:00:22 +00:00
{
auto& scriptEngine = GetContext()->GetScriptEngine();
auto& execInfo = scriptEngine.GetExecInfo();
if (!execInfo.IsGameStateMutable())
{
auto ctx = scriptEngine.GetContext();
duk_error(ctx, DUK_ERR_ERROR, "Game state is not mutable in this context.");
}
2020-02-23 02:00:22 +00:00
}
}
2020-02-23 12:55:48 +00:00
#endif