mirror of https://github.com/OpenRCT2/OpenRCT2.git
1773 lines
51 KiB
C++
1773 lines
51 KiB
C++
/*****************************************************************************
|
|
* Copyright (c) 2014-2023 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.
|
|
*****************************************************************************/
|
|
|
|
#ifdef ENABLE_SCRIPTING
|
|
|
|
# include "ScriptEngine.h"
|
|
|
|
# include "../PlatformEnvironment.h"
|
|
# include "../actions/CustomAction.h"
|
|
# include "../actions/GameAction.h"
|
|
# include "../actions/RideCreateAction.h"
|
|
# include "../actions/StaffHireNewAction.h"
|
|
# include "../config/Config.h"
|
|
# include "../core/EnumMap.hpp"
|
|
# include "../core/File.h"
|
|
# include "../core/FileScanner.h"
|
|
# include "../core/Path.hpp"
|
|
# include "../interface/InteractiveConsole.h"
|
|
# include "../platform/Platform.h"
|
|
# include "Duktape.hpp"
|
|
# include "bindings/entity/ScEntity.hpp"
|
|
# include "bindings/entity/ScGuest.hpp"
|
|
# include "bindings/entity/ScLitter.hpp"
|
|
# include "bindings/entity/ScPeep.hpp"
|
|
# include "bindings/entity/ScStaff.hpp"
|
|
# include "bindings/entity/ScVehicle.hpp"
|
|
# include "bindings/game/ScCheats.hpp"
|
|
# include "bindings/game/ScConsole.hpp"
|
|
# include "bindings/game/ScContext.hpp"
|
|
# include "bindings/game/ScDisposable.hpp"
|
|
# include "bindings/game/ScProfiler.hpp"
|
|
# include "bindings/network/ScNetwork.hpp"
|
|
# include "bindings/network/ScPlayer.hpp"
|
|
# include "bindings/network/ScPlayerGroup.hpp"
|
|
# include "bindings/network/ScSocket.hpp"
|
|
# include "bindings/object/ScObject.hpp"
|
|
# include "bindings/ride/ScRide.hpp"
|
|
# include "bindings/ride/ScRideStation.hpp"
|
|
# include "bindings/world/ScClimate.hpp"
|
|
# include "bindings/world/ScDate.hpp"
|
|
# include "bindings/world/ScMap.hpp"
|
|
# include "bindings/world/ScPark.hpp"
|
|
# include "bindings/world/ScParkMessage.hpp"
|
|
# include "bindings/world/ScScenario.hpp"
|
|
# include "bindings/world/ScTile.hpp"
|
|
# include "bindings/world/ScTileElement.hpp"
|
|
|
|
# include <iostream>
|
|
# include <memory>
|
|
# include <stdexcept>
|
|
# include <string>
|
|
|
|
using namespace OpenRCT2;
|
|
using namespace OpenRCT2::Scripting;
|
|
|
|
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, int32_t nestLevel)
|
|
{
|
|
if (nestLevel >= 8)
|
|
{
|
|
_ss << "[...]";
|
|
return;
|
|
}
|
|
|
|
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, nestLevel);
|
|
}
|
|
else
|
|
{
|
|
StringifyObject(val, canStartWithNewLine, nestLevel);
|
|
}
|
|
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, int32_t nestLevel)
|
|
{
|
|
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, nestLevel + 1);
|
|
}
|
|
}
|
|
_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;
|
|
}
|
|
|
|
if (duk_get_prop_index(_context, -1, i))
|
|
{
|
|
Stringify(DukValue::take_from_stack(_context), false, nestLevel + 1);
|
|
}
|
|
}
|
|
_ss << " ]";
|
|
PopIndent(2);
|
|
if (canStartWithNewLine)
|
|
{
|
|
PopIndent();
|
|
}
|
|
}
|
|
duk_pop(_context);
|
|
}
|
|
|
|
void StringifyObject(const DukValue& val, bool canStartWithNewLine, int32_t nestLevel)
|
|
{
|
|
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, nestLevel + 1);
|
|
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, nestLevel + 1);
|
|
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)
|
|
{
|
|
_ss << (val.as_bool() ? "true" : "false");
|
|
}
|
|
|
|
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, 0);
|
|
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(*this)
|
|
{
|
|
}
|
|
|
|
void ScriptEngine::Initialise()
|
|
{
|
|
if (_initialised)
|
|
throw std::runtime_error("Script engine already initialised.");
|
|
|
|
auto ctx = static_cast<duk_context*>(_context);
|
|
ScCheats::Register(ctx);
|
|
ScClimate::Register(ctx);
|
|
ScClimateState::Register(ctx);
|
|
ScConfiguration::Register(ctx);
|
|
ScConsole::Register(ctx);
|
|
ScContext::Register(ctx);
|
|
ScDate::Register(ctx);
|
|
ScDisposable::Register(ctx);
|
|
ScMap::Register(ctx);
|
|
ScNetwork::Register(ctx);
|
|
ScObject::Register(ctx);
|
|
ScSmallSceneryObject::Register(ctx);
|
|
ScPark::Register(ctx);
|
|
ScParkMessage::Register(ctx);
|
|
ScPlayer::Register(ctx);
|
|
ScPlayerGroup::Register(ctx);
|
|
ScProfiler::Register(ctx);
|
|
ScRide::Register(ctx);
|
|
ScRideStation::Register(ctx);
|
|
ScRideObject::Register(ctx);
|
|
ScRideObjectVehicle::Register(ctx);
|
|
ScTile::Register(ctx);
|
|
ScTileElement::Register(ctx);
|
|
ScTrackIterator::Register(ctx);
|
|
ScTrackSegment::Register(ctx);
|
|
ScEntity::Register(ctx);
|
|
ScLitter::Register(ctx);
|
|
ScVehicle::Register(ctx);
|
|
ScPeep::Register(ctx);
|
|
ScGuest::Register(ctx);
|
|
ScThought::Register(ctx);
|
|
# ifndef DISABLE_NETWORK
|
|
ScSocket::Register(ctx);
|
|
ScListener::Register(ctx);
|
|
# endif
|
|
ScScenario::Register(ctx);
|
|
ScScenarioObjective::Register(ctx);
|
|
ScPatrolArea::Register(ctx);
|
|
ScStaff::Register(ctx);
|
|
|
|
dukglue_register_global(ctx, std::make_shared<ScCheats>(), "cheats");
|
|
dukglue_register_global(ctx, std::make_shared<ScClimate>(), "climate");
|
|
dukglue_register_global(ctx, std::make_shared<ScConsole>(_console), "console");
|
|
dukglue_register_global(ctx, std::make_shared<ScContext>(_execInfo, _hookEngine), "context");
|
|
dukglue_register_global(ctx, std::make_shared<ScDate>(), "date");
|
|
dukglue_register_global(ctx, std::make_shared<ScMap>(ctx), "map");
|
|
dukglue_register_global(ctx, std::make_shared<ScNetwork>(ctx), "network");
|
|
dukglue_register_global(ctx, std::make_shared<ScPark>(), "park");
|
|
dukglue_register_global(ctx, std::make_shared<ScProfiler>(ctx), "profiler");
|
|
dukglue_register_global(ctx, std::make_shared<ScScenario>(), "scenario");
|
|
|
|
RegisterConstants();
|
|
|
|
_initialised = true;
|
|
_transientPluginsEnabled = false;
|
|
_transientPluginsStarted = false;
|
|
|
|
LoadSharedStorage();
|
|
ClearParkStorage();
|
|
}
|
|
|
|
class ConstantBuilder
|
|
{
|
|
private:
|
|
duk_context* _ctx;
|
|
DukValue _obj;
|
|
|
|
public:
|
|
ConstantBuilder(duk_context* ctx)
|
|
: _ctx(ctx)
|
|
{
|
|
duk_push_global_object(_ctx);
|
|
_obj = DukValue::take_from_stack(_ctx);
|
|
}
|
|
|
|
ConstantBuilder& Namespace(std::string_view ns)
|
|
{
|
|
auto flags = DUK_DEFPROP_ENUMERABLE | DUK_DEFPROP_HAVE_WRITABLE | DUK_DEFPROP_HAVE_ENUMERABLE
|
|
| DUK_DEFPROP_HAVE_CONFIGURABLE | DUK_DEFPROP_HAVE_VALUE;
|
|
|
|
// Create a new object for namespace
|
|
duk_push_global_object(_ctx);
|
|
duk_push_lstring(_ctx, ns.data(), ns.size());
|
|
duk_push_object(_ctx);
|
|
|
|
// Keep a reference to the namespace object
|
|
duk_dup_top(_ctx);
|
|
_obj = DukValue::take_from_stack(_ctx);
|
|
|
|
// Place the namespace object into the global context
|
|
duk_def_prop(_ctx, -3, flags);
|
|
duk_pop(_ctx);
|
|
|
|
return *this;
|
|
}
|
|
|
|
ConstantBuilder& Constant(std::string_view name, int32_t value)
|
|
{
|
|
auto flags = DUK_DEFPROP_ENUMERABLE | DUK_DEFPROP_HAVE_WRITABLE | DUK_DEFPROP_HAVE_ENUMERABLE
|
|
| DUK_DEFPROP_HAVE_CONFIGURABLE | DUK_DEFPROP_HAVE_VALUE;
|
|
_obj.push();
|
|
duk_push_lstring(_ctx, name.data(), name.size());
|
|
duk_push_int(_ctx, value);
|
|
duk_def_prop(_ctx, -3, flags);
|
|
duk_pop(_ctx);
|
|
return *this;
|
|
}
|
|
};
|
|
|
|
void ScriptEngine::RegisterConstants()
|
|
{
|
|
ConstantBuilder builder(_context);
|
|
builder.Namespace("TrackSlope")
|
|
.Constant("None", TRACK_SLOPE_NONE)
|
|
.Constant("Up25", TRACK_SLOPE_UP_25)
|
|
.Constant("Up60", TRACK_SLOPE_UP_60)
|
|
.Constant("Down25", TRACK_SLOPE_DOWN_25)
|
|
.Constant("Down60", TRACK_SLOPE_DOWN_60)
|
|
.Constant("Up90", TRACK_SLOPE_UP_90)
|
|
.Constant("Down90", TRACK_SLOPE_DOWN_90);
|
|
builder.Namespace("TrackBanking")
|
|
.Constant("None", TRACK_BANK_NONE)
|
|
.Constant("BankLeft", TRACK_BANK_LEFT)
|
|
.Constant("BankRight", TRACK_BANK_RIGHT)
|
|
.Constant("UpsideDown", TRACK_BANK_UPSIDE_DOWN);
|
|
}
|
|
|
|
void ScriptEngine::RefreshPlugins()
|
|
{
|
|
// Get a list of removed and added plugin files
|
|
auto pluginFiles = GetPluginFiles();
|
|
std::vector<std::string> plugins;
|
|
std::vector<std::string> removedPlugins;
|
|
std::vector<std::string> addedPlugins;
|
|
for (const auto& plugin : _plugins)
|
|
{
|
|
if (plugin->HasPath())
|
|
{
|
|
plugins.emplace_back(plugin->GetPath());
|
|
}
|
|
}
|
|
|
|
// The lists need to be sorted for std::set_difference to work properly
|
|
std::sort(pluginFiles.begin(), pluginFiles.end());
|
|
std::sort(plugins.begin(), plugins.end());
|
|
|
|
std::set_difference(
|
|
plugins.begin(), plugins.end(), pluginFiles.begin(), pluginFiles.end(), std::back_inserter(removedPlugins));
|
|
std::set_difference(
|
|
pluginFiles.begin(), pluginFiles.end(), plugins.begin(), plugins.end(), std::back_inserter(addedPlugins));
|
|
|
|
// Unregister plugin files that were removed
|
|
for (const auto& plugin : removedPlugins)
|
|
{
|
|
UnregisterPlugin(plugin);
|
|
}
|
|
|
|
// Register plugin files that were added
|
|
for (const auto& plugin : addedPlugins)
|
|
{
|
|
RegisterPlugin(plugin);
|
|
}
|
|
|
|
// Turn on hot reload if not already enabled
|
|
if (!_hotReloadingInitialised && gConfigPlugin.EnableHotReloading && network_get_mode() == NETWORK_MODE_NONE)
|
|
{
|
|
SetupHotReloading();
|
|
}
|
|
}
|
|
|
|
std::vector<std::string> ScriptEngine::GetPluginFiles() const
|
|
{
|
|
// Scan for .js files in plugin directory
|
|
std::vector<std::string> pluginFiles;
|
|
auto base = _env.GetDirectoryPath(DIRBASE::USER, DIRID::PLUGIN);
|
|
if (Path::DirectoryExists(base))
|
|
{
|
|
auto pattern = Path::Combine(base, u8"*.js");
|
|
auto scanner = Path::ScanDirectory(pattern, true);
|
|
while (scanner->Next())
|
|
{
|
|
auto path = std::string(scanner->GetPath());
|
|
if (ShouldLoadScript(path))
|
|
{
|
|
pluginFiles.push_back(path);
|
|
}
|
|
}
|
|
}
|
|
return pluginFiles;
|
|
}
|
|
|
|
bool ScriptEngine::ShouldLoadScript(std::string_view 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_view::npos && path.find("\\node_modules\\") == std::string_view::npos;
|
|
}
|
|
|
|
void ScriptEngine::UnregisterPlugin(std::string_view path)
|
|
{
|
|
try
|
|
{
|
|
auto pluginIt = std::find_if(_plugins.begin(), _plugins.end(), [path](const std::shared_ptr<Plugin>& plugin) {
|
|
return plugin->GetPath() == path;
|
|
});
|
|
auto& plugin = *pluginIt;
|
|
|
|
StopPlugin(plugin);
|
|
UnloadPlugin(plugin);
|
|
LogPluginInfo(plugin, "Unregistered");
|
|
|
|
_plugins.erase(pluginIt);
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
_console.WriteLineError(e.what());
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::RegisterPlugin(std::string_view path)
|
|
{
|
|
try
|
|
{
|
|
auto plugin = std::make_shared<Plugin>(_context, path);
|
|
|
|
// We must load the plugin to get the metadata for it
|
|
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
|
|
plugin->Load();
|
|
|
|
// Unload the plugin now, metadata is kept
|
|
plugin->Unload();
|
|
|
|
LogPluginInfo(plugin, "Registered");
|
|
_plugins.push_back(std::move(plugin));
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
_console.WriteLineError(e.what());
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::StartIntransientPlugins()
|
|
{
|
|
LoadSharedStorage();
|
|
|
|
for (auto& plugin : _plugins)
|
|
{
|
|
if (!plugin->HasStarted() && !plugin->IsTransient())
|
|
{
|
|
LoadPlugin(plugin);
|
|
StartPlugin(plugin);
|
|
}
|
|
}
|
|
|
|
_intransientPluginsStarted = true;
|
|
}
|
|
|
|
void ScriptEngine::StopUnloadRegisterAllPlugins()
|
|
{
|
|
std::vector<std::string> pluginPaths;
|
|
for (auto& plugin : _plugins)
|
|
{
|
|
pluginPaths.emplace_back(plugin->GetPath());
|
|
StopPlugin(plugin);
|
|
}
|
|
for (auto& plugin : _plugins)
|
|
{
|
|
UnloadPlugin(plugin);
|
|
}
|
|
for (auto& pluginPath : pluginPaths)
|
|
{
|
|
UnregisterPlugin(pluginPath);
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::LoadTransientPlugins()
|
|
{
|
|
if (!_initialised)
|
|
{
|
|
Initialise();
|
|
RefreshPlugins();
|
|
}
|
|
_transientPluginsEnabled = true;
|
|
}
|
|
|
|
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
|
|
{
|
|
if (!plugin->IsLoaded())
|
|
{
|
|
const auto& metadata = plugin->GetMetadata();
|
|
if (metadata.MinApiVersion <= OPENRCT2_PLUGIN_API_VERSION)
|
|
{
|
|
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
|
|
plugin->Load();
|
|
LogPluginInfo(plugin, "Loaded");
|
|
}
|
|
else
|
|
{
|
|
LogPluginInfo(plugin, "Requires newer API version: v" + std::to_string(metadata.MinApiVersion));
|
|
}
|
|
}
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
_console.WriteLineError(e.what());
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::UnloadPlugin(std::shared_ptr<Plugin>& plugin)
|
|
{
|
|
if (plugin->IsLoaded())
|
|
{
|
|
plugin->Unload();
|
|
LogPluginInfo(plugin, "Unloaded");
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::StartPlugin(std::shared_ptr<Plugin> plugin)
|
|
{
|
|
if (!plugin->HasStarted() && ShouldStartPlugin(plugin))
|
|
{
|
|
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
|
|
try
|
|
{
|
|
LogPluginInfo(plugin, "Started");
|
|
plugin->Start();
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
_console.WriteLineError(e.what());
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::StopPlugin(std::shared_ptr<Plugin> plugin)
|
|
{
|
|
if (plugin->HasStarted())
|
|
{
|
|
plugin->StopBegin();
|
|
|
|
for (const auto& callback : _pluginStoppedSubscriptions)
|
|
{
|
|
callback(plugin);
|
|
}
|
|
RemoveCustomGameActions(plugin);
|
|
RemoveIntervals(plugin);
|
|
RemoveSockets(plugin);
|
|
_hookEngine.UnsubscribeAll(plugin);
|
|
|
|
plugin->StopEnd();
|
|
LogPluginInfo(plugin, "Stopped");
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::ReloadPlugin(std::shared_ptr<Plugin> plugin)
|
|
{
|
|
StopPlugin(plugin);
|
|
{
|
|
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false);
|
|
plugin->Load();
|
|
LogPluginInfo(plugin, "Reloaded");
|
|
}
|
|
StartPlugin(plugin);
|
|
}
|
|
|
|
void ScriptEngine::SetupHotReloading()
|
|
{
|
|
try
|
|
{
|
|
auto base = _env.GetDirectoryPath(DIRBASE::USER, DIRID::PLUGIN);
|
|
if (Path::DirectoryExists(base))
|
|
{
|
|
_pluginFileWatcher = std::make_unique<FileWatcher>(base);
|
|
_pluginFileWatcher->OnFileChanged = [this](u8string_view path) {
|
|
std::lock_guard guard(_changedPluginFilesMutex);
|
|
_changedPluginFiles.emplace(path);
|
|
};
|
|
_hotReloadingInitialised = true;
|
|
}
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
Console::Error::WriteLine("Unable to enable hot reloading of plugins: %s", e.what());
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::DoAutoReloadPluginCheck()
|
|
{
|
|
if (_hotReloadingInitialised)
|
|
{
|
|
auto tick = Platform::GetTicks();
|
|
if (tick - _lastHotReloadCheckTick > 1000)
|
|
{
|
|
AutoReloadPlugins();
|
|
_lastHotReloadCheckTick = tick;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::AutoReloadPlugins()
|
|
{
|
|
if (!_changedPluginFiles.empty())
|
|
{
|
|
std::lock_guard guard(_changedPluginFilesMutex);
|
|
for (const auto& path : _changedPluginFiles)
|
|
{
|
|
auto findResult = std::find_if(_plugins.begin(), _plugins.end(), [&path](const std::shared_ptr<Plugin>& plugin) {
|
|
return Path::Equals(path, plugin->GetPath());
|
|
});
|
|
if (findResult != _plugins.end())
|
|
{
|
|
auto& plugin = *findResult;
|
|
try
|
|
{
|
|
ReloadPlugin(plugin);
|
|
}
|
|
catch (const std::exception& e)
|
|
{
|
|
_console.WriteLineError(e.what());
|
|
}
|
|
}
|
|
}
|
|
_changedPluginFiles.clear();
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::UnloadTransientPlugins()
|
|
{
|
|
// Stop them all first
|
|
for (auto& plugin : _plugins)
|
|
{
|
|
if (plugin->IsTransient())
|
|
{
|
|
StopPlugin(plugin);
|
|
}
|
|
}
|
|
|
|
// Now unload them
|
|
for (auto& plugin : _plugins)
|
|
{
|
|
if (plugin->IsTransient())
|
|
{
|
|
UnloadPlugin(plugin);
|
|
}
|
|
}
|
|
|
|
_transientPluginsEnabled = false;
|
|
_transientPluginsStarted = false;
|
|
}
|
|
|
|
void ScriptEngine::StartTransientPlugins()
|
|
{
|
|
LoadSharedStorage();
|
|
|
|
// Load transient plugins
|
|
for (auto& plugin : _plugins)
|
|
{
|
|
if (plugin->IsTransient() && !plugin->IsLoaded() && ShouldStartPlugin(plugin))
|
|
{
|
|
LoadPlugin(plugin);
|
|
}
|
|
}
|
|
|
|
// Start transient plugins
|
|
for (auto& plugin : _plugins)
|
|
{
|
|
if (plugin->IsTransient() && plugin->IsLoaded() && !plugin->HasStarted())
|
|
{
|
|
StartPlugin(plugin);
|
|
}
|
|
}
|
|
|
|
_transientPluginsStarted = 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;
|
|
}
|
|
|
|
void ScriptEngine::Tick()
|
|
{
|
|
PROFILED_FUNCTION();
|
|
|
|
CheckAndStartPlugins();
|
|
UpdateIntervals();
|
|
UpdateSockets();
|
|
ProcessREPL();
|
|
DoAutoReloadPluginCheck();
|
|
}
|
|
|
|
void ScriptEngine::CheckAndStartPlugins()
|
|
{
|
|
auto startIntransient = !_intransientPluginsStarted;
|
|
auto startTransient = !_transientPluginsStarted && _transientPluginsEnabled;
|
|
|
|
if (startIntransient || startTransient)
|
|
{
|
|
RefreshPlugins();
|
|
}
|
|
if (startIntransient)
|
|
{
|
|
StartIntransientPlugins();
|
|
}
|
|
if (startTransient)
|
|
{
|
|
StartTransientPlugins();
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
else if (duk_get_type(_context, -1) != DUK_TYPE_UNDEFINED)
|
|
{
|
|
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;
|
|
}
|
|
|
|
DukValue ScriptEngine::ExecutePluginCall(
|
|
const std::shared_ptr<Plugin>& plugin, const DukValue& func, const std::vector<DukValue>& args, bool isGameStateMutable)
|
|
{
|
|
duk_push_undefined(_context);
|
|
auto dukUndefined = DukValue::take_from_stack(_context);
|
|
return ExecutePluginCall(plugin, func, dukUndefined, args, isGameStateMutable);
|
|
}
|
|
|
|
// Must pass plugin by-value, a JS function could destroy the original reference
|
|
DukValue ScriptEngine::ExecutePluginCall(
|
|
std::shared_ptr<Plugin> plugin, const DukValue& func, const DukValue& thisValue, const std::vector<DukValue>& args,
|
|
bool isGameStateMutable)
|
|
{
|
|
DukStackFrame frame(_context);
|
|
if (func.is_function() && plugin->HasStarted())
|
|
{
|
|
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, isGameStateMutable);
|
|
func.push();
|
|
thisValue.push();
|
|
for (const auto& arg : args)
|
|
{
|
|
arg.push();
|
|
}
|
|
auto result = duk_pcall_method(_context, static_cast<duk_idx_t>(args.size()));
|
|
if (result == DUK_EXEC_SUCCESS)
|
|
{
|
|
return DukValue::take_from_stack(_context);
|
|
}
|
|
|
|
auto message = duk_safe_to_string(_context, -1);
|
|
LogPluginInfo(plugin, message);
|
|
duk_pop(_context);
|
|
}
|
|
return DukValue();
|
|
}
|
|
|
|
void ScriptEngine::LogPluginInfo(std::string_view message)
|
|
{
|
|
auto plugin = _execInfo.GetCurrentPlugin();
|
|
LogPluginInfo(plugin, message);
|
|
}
|
|
|
|
void ScriptEngine::LogPluginInfo(const std::shared_ptr<Plugin>& plugin, std::string_view message)
|
|
{
|
|
if (plugin == nullptr)
|
|
{
|
|
_console.WriteLine(std::string(message));
|
|
}
|
|
else
|
|
{
|
|
const auto& pluginName = plugin->GetMetadata().Name;
|
|
_console.WriteLine("[" + pluginName + "] " + std::string(message));
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::AddNetworkPlugin(std::string_view code)
|
|
{
|
|
auto plugin = std::make_shared<Plugin>(_context, std::string());
|
|
plugin->SetCode(code);
|
|
_plugins.push_back(plugin);
|
|
}
|
|
|
|
void ScriptEngine::RemoveNetworkPlugins()
|
|
{
|
|
auto it = _plugins.begin();
|
|
while (it != _plugins.end())
|
|
{
|
|
auto plugin = (*it);
|
|
if (!plugin->HasPath())
|
|
{
|
|
StopPlugin(plugin);
|
|
UnloadPlugin(plugin);
|
|
LogPluginInfo(plugin, "Unregistered network plugin");
|
|
|
|
it = _plugins.erase(it);
|
|
}
|
|
else
|
|
{
|
|
it++;
|
|
}
|
|
}
|
|
}
|
|
|
|
GameActions::Result ScriptEngine::QueryOrExecuteCustomGameAction(const CustomAction& customAction, bool isExecute)
|
|
{
|
|
std::string actionz = customAction.GetId();
|
|
auto kvp = _customActions.find(actionz);
|
|
if (kvp != _customActions.end())
|
|
{
|
|
const auto& customActionInfo = kvp->second;
|
|
|
|
// Deserialise the JSON args
|
|
std::string argsz = customAction.GetJson();
|
|
|
|
auto dukArgs = DuktapeTryParseJson(_context, argsz);
|
|
if (!dukArgs)
|
|
{
|
|
auto action = GameActions::Result();
|
|
action.Error = GameActions::Status::InvalidParameters;
|
|
action.ErrorTitle = "Invalid JSON";
|
|
return action;
|
|
}
|
|
|
|
std::vector<DukValue> pluginCallArgs;
|
|
if (GetTargetAPIVersion() <= API_VERSION_68_CUSTOM_ACTION_ARGS)
|
|
{
|
|
pluginCallArgs = { *dukArgs };
|
|
}
|
|
else
|
|
{
|
|
DukObject obj(_context);
|
|
obj.Set("action", actionz);
|
|
obj.Set("args", *dukArgs);
|
|
obj.Set("player", customAction.GetPlayer());
|
|
obj.Set("type", EnumValue(customAction.GetType()));
|
|
|
|
auto flags = customAction.GetActionFlags();
|
|
obj.Set("isClientOnly", (flags & GameActions::Flags::ClientOnly) != 0);
|
|
pluginCallArgs = { obj.Take() };
|
|
}
|
|
|
|
// Ready to call plugin handler
|
|
DukValue dukResult;
|
|
if (!isExecute)
|
|
{
|
|
dukResult = ExecutePluginCall(customActionInfo.Owner, customActionInfo.Query, pluginCallArgs, false);
|
|
}
|
|
else
|
|
{
|
|
dukResult = ExecutePluginCall(customActionInfo.Owner, customActionInfo.Execute, pluginCallArgs, true);
|
|
}
|
|
return DukToGameActionResult(dukResult);
|
|
}
|
|
|
|
auto action = GameActions::Result();
|
|
action.Error = GameActions::Status::Unknown;
|
|
action.ErrorTitle = "Unknown custom action";
|
|
return action;
|
|
}
|
|
|
|
GameActions::Result ScriptEngine::DukToGameActionResult(const DukValue& d)
|
|
{
|
|
auto result = GameActions::Result();
|
|
result.Error = static_cast<GameActions::Status>(AsOrDefault<int32_t>(d["error"]));
|
|
result.ErrorTitle = AsOrDefault<std::string>(d["errorTitle"]);
|
|
result.ErrorMessage = AsOrDefault<std::string>(d["errorMessage"]);
|
|
result.Cost = AsOrDefault<int32_t>(d["cost"]);
|
|
|
|
auto expenditureType = AsOrDefault<std::string>(d["expenditureType"]);
|
|
if (!expenditureType.empty())
|
|
{
|
|
auto expenditure = StringToExpenditureType(expenditureType);
|
|
if (expenditure != ExpenditureType::Count)
|
|
{
|
|
result.Expenditure = expenditure;
|
|
}
|
|
}
|
|
return result;
|
|
}
|
|
|
|
constexpr static const char* ExpenditureTypes[] = {
|
|
"ride_construction",
|
|
"ride_runningcosts",
|
|
"land_purchase",
|
|
"landscaping",
|
|
"park_entrance_tickets",
|
|
"park_ride_tickets",
|
|
"shop_sales",
|
|
"shop_stock",
|
|
"food_drink_sales",
|
|
"food_drink_stock",
|
|
"wages",
|
|
"marketing",
|
|
"research",
|
|
"interest",
|
|
};
|
|
|
|
std::string_view ScriptEngine::ExpenditureTypeToString(ExpenditureType expenditureType)
|
|
{
|
|
auto index = static_cast<size_t>(expenditureType);
|
|
if (index < std::size(ExpenditureTypes))
|
|
{
|
|
return ExpenditureTypes[index];
|
|
}
|
|
return {};
|
|
}
|
|
|
|
ExpenditureType ScriptEngine::StringToExpenditureType(std::string_view expenditureType)
|
|
{
|
|
auto it = std::find(std::begin(ExpenditureTypes), std::end(ExpenditureTypes), expenditureType);
|
|
if (it != std::end(ExpenditureTypes))
|
|
{
|
|
return static_cast<ExpenditureType>(std::distance(std::begin(ExpenditureTypes), it));
|
|
}
|
|
return ExpenditureType::Count;
|
|
}
|
|
|
|
DukValue ScriptEngine::GameActionResultToDuk(const GameAction& action, const GameActions::Result& result)
|
|
{
|
|
DukStackFrame frame(_context);
|
|
DukObject obj(_context);
|
|
|
|
obj.Set("error", static_cast<duk_int_t>(result.Error));
|
|
if (result.Error != GameActions::Status::Ok)
|
|
{
|
|
obj.Set("errorTitle", result.GetErrorTitle());
|
|
obj.Set("errorMessage", result.GetErrorMessage());
|
|
}
|
|
|
|
if (result.Cost != MONEY32_UNDEFINED)
|
|
{
|
|
obj.Set("cost", result.Cost);
|
|
}
|
|
if (!result.Position.IsNull())
|
|
{
|
|
obj.Set("position", ToDuk(_context, result.Position));
|
|
}
|
|
if (result.Expenditure != ExpenditureType::Count)
|
|
{
|
|
obj.Set("expenditureType", ExpenditureTypeToString(result.Expenditure));
|
|
}
|
|
|
|
// RideCreateAction only
|
|
if (action.GetType() == GameCommand::CreateRide)
|
|
{
|
|
if (result.Error == GameActions::Status::Ok)
|
|
{
|
|
const auto rideIndex = result.GetData<RideId>();
|
|
obj.Set("ride", rideIndex.ToUnderlying());
|
|
}
|
|
}
|
|
// StaffHireNewAction only
|
|
else if (action.GetType() == GameCommand::HireNewStaffMember)
|
|
{
|
|
if (result.Error == GameActions::Status::Ok)
|
|
{
|
|
const auto actionResult = result.GetData<StaffHireNewActionResult>();
|
|
if (!actionResult.StaffEntityId.IsNull())
|
|
{
|
|
obj.Set("peep", actionResult.StaffEntityId.ToUnderlying());
|
|
}
|
|
}
|
|
}
|
|
|
|
return obj.Take();
|
|
}
|
|
|
|
bool ScriptEngine::RegisterCustomAction(
|
|
const std::shared_ptr<Plugin>& plugin, std::string_view action, const DukValue& query, const DukValue& execute)
|
|
{
|
|
std::string actionz = std::string(action);
|
|
if (_customActions.find(actionz) != _customActions.end())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
CustomActionInfo customAction;
|
|
customAction.Owner = plugin;
|
|
customAction.Name = std::move(actionz);
|
|
customAction.Query = query;
|
|
customAction.Execute = execute;
|
|
_customActions[customAction.Name] = std::move(customAction);
|
|
return true;
|
|
}
|
|
|
|
void ScriptEngine::RemoveCustomGameActions(const std::shared_ptr<Plugin>& plugin)
|
|
{
|
|
for (auto it = _customActions.begin(); it != _customActions.end();)
|
|
{
|
|
if (it->second.Owner == plugin)
|
|
{
|
|
it = _customActions.erase(it);
|
|
}
|
|
else
|
|
{
|
|
it++;
|
|
}
|
|
}
|
|
}
|
|
|
|
class DukToGameActionParameterVisitor : public GameActionParameterVisitor
|
|
{
|
|
private:
|
|
DukValue _dukValue;
|
|
|
|
public:
|
|
DukToGameActionParameterVisitor(DukValue&& dukValue)
|
|
: _dukValue(std::move(dukValue))
|
|
{
|
|
}
|
|
|
|
void Visit(std::string_view name, bool& param) override
|
|
{
|
|
param = _dukValue[name].as_bool();
|
|
}
|
|
|
|
void Visit(std::string_view name, int32_t& param) override
|
|
{
|
|
param = _dukValue[name].as_int();
|
|
}
|
|
|
|
void Visit(std::string_view name, std::string& param) override
|
|
{
|
|
param = _dukValue[name].as_string();
|
|
}
|
|
};
|
|
|
|
class DukFromGameActionParameterVisitor : public GameActionParameterVisitor
|
|
{
|
|
private:
|
|
DukObject& _dukObject;
|
|
|
|
public:
|
|
DukFromGameActionParameterVisitor(DukObject& dukObject)
|
|
: _dukObject(dukObject)
|
|
{
|
|
}
|
|
|
|
void Visit(std::string_view name, bool& param) override
|
|
{
|
|
std::string szName(name);
|
|
_dukObject.Set(szName.c_str(), param);
|
|
}
|
|
|
|
void Visit(std::string_view name, int32_t& param) override
|
|
{
|
|
std::string szName(name);
|
|
_dukObject.Set(szName.c_str(), param);
|
|
}
|
|
|
|
void Visit(std::string_view name, std::string& param) override
|
|
{
|
|
std::string szName(name);
|
|
_dukObject.Set(szName.c_str(), param);
|
|
}
|
|
};
|
|
|
|
// clang-format off
|
|
const static EnumMap<GameCommand> ActionNameToType = {
|
|
{ "balloonpress", GameCommand::BalloonPress },
|
|
{ "bannerplace", GameCommand::PlaceBanner },
|
|
{ "bannerremove", GameCommand::RemoveBanner },
|
|
{ "bannersetcolour", GameCommand::SetBannerColour },
|
|
{ "bannersetname", GameCommand::SetBannerName },
|
|
{ "bannersetstyle", GameCommand::SetBannerStyle },
|
|
{ "clearscenery", GameCommand::ClearScenery },
|
|
{ "climateset", GameCommand::SetClimate },
|
|
{ "footpathplace", GameCommand::PlacePath },
|
|
{ "footpathlayoutplace", GameCommand::PlacePathLayout },
|
|
{ "footpathremove", GameCommand::RemovePath },
|
|
{ "footpathadditionplace", GameCommand::PlaceFootpathAddition },
|
|
{ "footpathadditionremove", GameCommand::RemoveFootpathAddition },
|
|
{ "guestsetflags", GameCommand::GuestSetFlags },
|
|
{ "guestsetname", GameCommand::SetGuestName },
|
|
{ "landbuyrights", GameCommand::BuyLandRights },
|
|
{ "landlower", GameCommand::LowerLand },
|
|
{ "landraise", GameCommand::RaiseLand },
|
|
{ "landsetheight", GameCommand::SetLandHeight },
|
|
{ "landsetrights", GameCommand::SetLandOwnership },
|
|
{ "landsmooth", GameCommand::EditLandSmooth },
|
|
{ "largesceneryplace", GameCommand::PlaceLargeScenery },
|
|
{ "largesceneryremove", GameCommand::RemoveLargeScenery },
|
|
{ "largescenerysetcolour", GameCommand::SetLargeSceneryColour },
|
|
{ "loadorquit", GameCommand::LoadOrQuit },
|
|
{ "mapchangesize", GameCommand::ChangeMapSize },
|
|
{ "mazeplacetrack", GameCommand::PlaceMazeDesign },
|
|
{ "mazesettrack", GameCommand::SetMazeTrack },
|
|
{ "networkmodifygroup", GameCommand::ModifyGroups },
|
|
{ "parkentranceplace", GameCommand::PlaceParkEntrance },
|
|
{ "parkentranceremove", GameCommand::RemoveParkEntrance },
|
|
{ "parkmarketing", GameCommand::StartMarketingCampaign },
|
|
{ "parksetdate", GameCommand::SetDate },
|
|
{ "parksetentrancefee", GameCommand::SetParkEntranceFee },
|
|
{ "parksetloan", GameCommand::SetCurrentLoan },
|
|
{ "parksetname", GameCommand::SetParkName },
|
|
{ "parksetparameter", GameCommand::SetParkOpen },
|
|
{ "parksetresearchfunding", GameCommand::SetResearchFunding },
|
|
{ "pausetoggle", GameCommand::TogglePause },
|
|
{ "peeppickup", GameCommand::PickupGuest },
|
|
{ "peepspawnplace", GameCommand::PlacePeepSpawn },
|
|
{ "playerkick", GameCommand::KickPlayer },
|
|
{ "playersetgroup", GameCommand::SetPlayerGroup },
|
|
{ "ridecreate", GameCommand::CreateRide },
|
|
{ "ridedemolish", GameCommand::DemolishRide },
|
|
{ "rideentranceexitplace", GameCommand::PlaceRideEntranceOrExit },
|
|
{ "rideentranceexitremove", GameCommand::RemoveRideEntranceOrExit },
|
|
{ "ridefreezerating", GameCommand::FreezeRideRating },
|
|
{ "ridesetappearance", GameCommand::SetRideAppearance },
|
|
{ "ridesetcolourscheme", GameCommand::SetColourScheme },
|
|
{ "ridesetname", GameCommand::SetRideName },
|
|
{ "ridesetprice", GameCommand::SetRidePrice },
|
|
{ "ridesetsetting", GameCommand::SetRideSetting },
|
|
{ "ridesetstatus", GameCommand::SetRideStatus },
|
|
{ "ridesetvehicle", GameCommand::SetRideVehicles },
|
|
{ "scenariosetsetting", GameCommand::EditScenarioOptions },
|
|
{ "setcheat", GameCommand::Cheat },
|
|
{ "signsetname", GameCommand::SetSignName },
|
|
{ "signsetstyle", GameCommand::SetSignStyle },
|
|
{ "smallsceneryplace", GameCommand::PlaceScenery },
|
|
{ "smallsceneryremove", GameCommand::RemoveScenery },
|
|
{ "smallscenerysetcolour", GameCommand::SetSceneryColour},
|
|
{ "stafffire", GameCommand::FireStaffMember },
|
|
{ "staffhire", GameCommand::HireNewStaffMember },
|
|
{ "staffsetcolour", GameCommand::SetStaffColour },
|
|
{ "staffsetcostume", GameCommand::SetStaffCostume },
|
|
{ "staffsetname", GameCommand::SetStaffName },
|
|
{ "staffsetorders", GameCommand::SetStaffOrders },
|
|
{ "staffsetpatrolarea", GameCommand::SetStaffPatrol },
|
|
{ "surfacesetstyle", GameCommand::ChangeSurfaceStyle },
|
|
{ "tilemodify", GameCommand::ModifyTile },
|
|
{ "trackdesign", GameCommand::PlaceTrackDesign },
|
|
{ "trackplace", GameCommand::PlaceTrack },
|
|
{ "trackremove", GameCommand::RemoveTrack },
|
|
{ "tracksetbrakespeed", GameCommand::SetBrakesSpeed },
|
|
{ "wallplace", GameCommand::PlaceWall },
|
|
{ "wallremove", GameCommand::RemoveWall },
|
|
{ "wallsetcolour", GameCommand::SetWallColour },
|
|
{ "waterlower", GameCommand::LowerWater },
|
|
{ "waterraise", GameCommand::RaiseWater },
|
|
{ "watersetheight", GameCommand::SetWaterHeight }
|
|
};
|
|
// clang-format on
|
|
|
|
static std::string GetActionName(GameCommand commandId)
|
|
{
|
|
auto it = ActionNameToType.find(commandId);
|
|
if (it != ActionNameToType.end())
|
|
{
|
|
return std::string{ it->first };
|
|
}
|
|
return {};
|
|
}
|
|
|
|
static std::unique_ptr<GameAction> CreateGameActionFromActionId(const std::string& name)
|
|
{
|
|
auto result = ActionNameToType.find(name);
|
|
if (result != ActionNameToType.end())
|
|
{
|
|
return GameActions::Create(result->second);
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
void ScriptEngine::RunGameActionHooks(const GameAction& action, GameActions::Result& result, bool isExecute)
|
|
{
|
|
DukStackFrame frame(_context);
|
|
|
|
auto hookType = isExecute ? HOOK_TYPE::ACTION_EXECUTE : HOOK_TYPE::ACTION_QUERY;
|
|
if (_hookEngine.HasSubscriptions(hookType))
|
|
{
|
|
DukObject obj(_context);
|
|
|
|
auto actionId = action.GetType();
|
|
if (action.GetType() == GameCommand::Custom)
|
|
{
|
|
auto customAction = static_cast<const CustomAction&>(action);
|
|
obj.Set("action", customAction.GetId());
|
|
|
|
auto dukArgs = DuktapeTryParseJson(_context, customAction.GetJson());
|
|
if (dukArgs)
|
|
{
|
|
obj.Set("args", *dukArgs);
|
|
}
|
|
else
|
|
{
|
|
DukObject args(_context);
|
|
obj.Set("args", args.Take());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
auto actionName = GetActionName(actionId);
|
|
if (!actionName.empty())
|
|
{
|
|
obj.Set("action", actionName);
|
|
}
|
|
|
|
DukObject args(_context);
|
|
DukFromGameActionParameterVisitor visitor(args);
|
|
const_cast<GameAction&>(action).AcceptParameters(visitor);
|
|
const_cast<GameAction&>(action).AcceptFlags(visitor);
|
|
obj.Set("args", args.Take());
|
|
}
|
|
|
|
obj.Set("player", action.GetPlayer());
|
|
obj.Set("type", EnumValue(actionId));
|
|
|
|
auto flags = action.GetActionFlags();
|
|
obj.Set("isClientOnly", (flags & GameActions::Flags::ClientOnly) != 0);
|
|
|
|
obj.Set("result", GameActionResultToDuk(action, result));
|
|
auto dukEventArgs = obj.Take();
|
|
|
|
_hookEngine.Call(hookType, dukEventArgs, false);
|
|
|
|
if (!isExecute)
|
|
{
|
|
auto dukResult = dukEventArgs["result"];
|
|
if (dukResult.type() == DukValue::Type::OBJECT)
|
|
{
|
|
auto error = AsOrDefault<int32_t>(dukResult["error"]);
|
|
if (error != 0)
|
|
{
|
|
result.Error = static_cast<GameActions::Status>(error);
|
|
result.ErrorTitle = AsOrDefault<std::string>(dukResult["errorTitle"]);
|
|
result.ErrorMessage = AsOrDefault<std::string>(dukResult["errorMessage"]);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::unique_ptr<GameAction> ScriptEngine::CreateGameAction(const std::string& actionid, const DukValue& args)
|
|
{
|
|
auto action = CreateGameActionFromActionId(actionid);
|
|
if (action != nullptr)
|
|
{
|
|
DukValue argsCopy = args;
|
|
DukToGameActionParameterVisitor visitor(std::move(argsCopy));
|
|
action->AcceptParameters(visitor);
|
|
if (args["flags"].type() == DukValue::Type::NUMBER)
|
|
{
|
|
action->AcceptFlags(visitor);
|
|
}
|
|
return action;
|
|
}
|
|
|
|
// Serialise args to json so that it can be sent
|
|
auto ctx = args.context();
|
|
if (args.type() == DukValue::Type::OBJECT)
|
|
{
|
|
args.push();
|
|
}
|
|
else
|
|
{
|
|
duk_push_object(ctx);
|
|
}
|
|
auto jsonz = duk_json_encode(ctx, -1);
|
|
auto json = std::string(jsonz);
|
|
duk_pop(ctx);
|
|
auto customAction = std::make_unique<CustomAction>(actionid, json);
|
|
|
|
if (customAction->GetPlayer() == -1 && network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
customAction->SetPlayer(network_get_current_player_id());
|
|
}
|
|
return customAction;
|
|
}
|
|
|
|
void ScriptEngine::InitSharedStorage()
|
|
{
|
|
duk_push_object(_context);
|
|
_sharedStorage = std::move(DukValue::take_from_stack(_context));
|
|
}
|
|
|
|
void ScriptEngine::LoadSharedStorage()
|
|
{
|
|
InitSharedStorage();
|
|
|
|
auto path = _env.GetFilePath(PATHID::PLUGIN_STORE);
|
|
try
|
|
{
|
|
if (File::Exists(path))
|
|
{
|
|
auto data = File::ReadAllBytes(path);
|
|
auto result = DuktapeTryParseJson(
|
|
_context, std::string_view(reinterpret_cast<const char*>(data.data()), data.size()));
|
|
if (result)
|
|
{
|
|
_sharedStorage = std::move(*result);
|
|
}
|
|
}
|
|
}
|
|
catch (const std::exception&)
|
|
{
|
|
Console::Error::WriteLine("Unable to read '%s'", path.c_str());
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::SaveSharedStorage()
|
|
{
|
|
auto path = _env.GetFilePath(PATHID::PLUGIN_STORE);
|
|
try
|
|
{
|
|
_sharedStorage.push();
|
|
auto json = std::string(duk_json_encode(_context, -1));
|
|
duk_pop(_context);
|
|
|
|
File::WriteAllBytes(path, json.c_str(), json.size());
|
|
}
|
|
catch (const std::exception&)
|
|
{
|
|
Console::Error::WriteLine("Unable to write to '%s'", path.c_str());
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::ClearParkStorage()
|
|
{
|
|
duk_push_object(_context);
|
|
_parkStorage = std::move(DukValue::take_from_stack(_context));
|
|
}
|
|
|
|
std::string ScriptEngine::GetParkStorageAsJSON()
|
|
{
|
|
_parkStorage.push();
|
|
auto json = std::string(duk_json_encode(_context, -1));
|
|
duk_pop(_context);
|
|
return json;
|
|
}
|
|
|
|
void ScriptEngine::SetParkStorageFromJSON(std::string_view value)
|
|
{
|
|
auto result = DuktapeTryParseJson(_context, value);
|
|
if (result)
|
|
{
|
|
_parkStorage = std::move(*result);
|
|
}
|
|
}
|
|
|
|
IntervalHandle ScriptEngine::AllocateHandle()
|
|
{
|
|
for (size_t i = 0; i < _intervals.size(); i++)
|
|
{
|
|
if (!_intervals[i].IsValid())
|
|
{
|
|
return static_cast<IntervalHandle>(i + 1);
|
|
}
|
|
}
|
|
_intervals.emplace_back();
|
|
return static_cast<IntervalHandle>(_intervals.size());
|
|
}
|
|
|
|
IntervalHandle ScriptEngine::AddInterval(const std::shared_ptr<Plugin>& plugin, int32_t delay, bool repeat, DukValue&& callback)
|
|
{
|
|
auto handle = AllocateHandle();
|
|
if (handle != 0)
|
|
{
|
|
auto& interval = _intervals[static_cast<size_t>(handle) - 1];
|
|
interval.Owner = plugin;
|
|
interval.Handle = handle;
|
|
interval.Delay = delay;
|
|
interval.LastTimestamp = _lastIntervalTimestamp;
|
|
interval.Callback = std::move(callback);
|
|
interval.Repeat = repeat;
|
|
}
|
|
return handle;
|
|
}
|
|
|
|
void ScriptEngine::RemoveInterval(const std::shared_ptr<Plugin>& plugin, IntervalHandle handle)
|
|
{
|
|
if (handle > 0 && static_cast<size_t>(handle) <= _intervals.size())
|
|
{
|
|
auto& interval = _intervals[static_cast<size_t>(handle) - 1];
|
|
|
|
// Only allow owner or REPL (nullptr) to remove intervals
|
|
if (plugin == nullptr || interval.Owner == plugin)
|
|
{
|
|
interval = {};
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::UpdateIntervals()
|
|
{
|
|
uint32_t timestamp = Platform::GetTicks();
|
|
if (timestamp < _lastIntervalTimestamp)
|
|
{
|
|
// timestamp has wrapped, subtract all intervals by the remaining amount before wrap
|
|
auto delta = static_cast<int64_t>(std::numeric_limits<uint32_t>::max() - _lastIntervalTimestamp);
|
|
for (auto& interval : _intervals)
|
|
{
|
|
if (interval.IsValid())
|
|
{
|
|
interval.LastTimestamp = -delta;
|
|
}
|
|
}
|
|
}
|
|
_lastIntervalTimestamp = timestamp;
|
|
|
|
for (auto& interval : _intervals)
|
|
{
|
|
if (interval.IsValid())
|
|
{
|
|
if (timestamp >= interval.LastTimestamp + interval.Delay)
|
|
{
|
|
ExecutePluginCall(interval.Owner, interval.Callback, {}, false);
|
|
|
|
interval.LastTimestamp = timestamp;
|
|
if (!interval.Repeat)
|
|
{
|
|
RemoveInterval(nullptr, interval.Handle);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScriptEngine::RemoveIntervals(const std::shared_ptr<Plugin>& plugin)
|
|
{
|
|
for (auto& interval : _intervals)
|
|
{
|
|
if (interval.Owner == plugin)
|
|
{
|
|
interval = {};
|
|
}
|
|
}
|
|
}
|
|
|
|
# ifndef DISABLE_NETWORK
|
|
void ScriptEngine::AddSocket(const std::shared_ptr<ScSocketBase>& socket)
|
|
{
|
|
_sockets.push_back(socket);
|
|
}
|
|
# endif
|
|
|
|
void ScriptEngine::UpdateSockets()
|
|
{
|
|
# ifndef DISABLE_NETWORK
|
|
// Use simple for i loop as Update calls can modify the list
|
|
auto it = _sockets.begin();
|
|
while (it != _sockets.end())
|
|
{
|
|
auto& socket = *it;
|
|
socket->Update();
|
|
if (socket->IsDisposed())
|
|
{
|
|
it = _sockets.erase(it);
|
|
}
|
|
else
|
|
{
|
|
it++;
|
|
}
|
|
}
|
|
# endif
|
|
}
|
|
|
|
void ScriptEngine::RemoveSockets(const std::shared_ptr<Plugin>& plugin)
|
|
{
|
|
# ifndef DISABLE_NETWORK
|
|
auto it = _sockets.begin();
|
|
while (it != _sockets.end())
|
|
{
|
|
auto socket = it->get();
|
|
if (socket->GetPlugin() == plugin)
|
|
{
|
|
socket->Dispose();
|
|
it = _sockets.erase(it);
|
|
}
|
|
else
|
|
{
|
|
it++;
|
|
}
|
|
}
|
|
# endif
|
|
}
|
|
|
|
std::string OpenRCT2::Scripting::Stringify(const DukValue& val)
|
|
{
|
|
return ExpressionStringifier::StringifyExpression(val);
|
|
}
|
|
|
|
std::string OpenRCT2::Scripting::ProcessString(const DukValue& value)
|
|
{
|
|
if (value.type() == DukValue::Type::STRING)
|
|
return value.as_string();
|
|
return {};
|
|
}
|
|
|
|
bool OpenRCT2::Scripting::IsGameStateMutable()
|
|
{
|
|
// Allow single player to alter game state anywhere
|
|
if (network_get_mode() == NETWORK_MODE_NONE)
|
|
{
|
|
return true;
|
|
}
|
|
|
|
auto& scriptEngine = GetContext()->GetScriptEngine();
|
|
auto& execInfo = scriptEngine.GetExecInfo();
|
|
return execInfo.IsGameStateMutable();
|
|
}
|
|
|
|
void OpenRCT2::Scripting::ThrowIfGameStateNotMutable()
|
|
{
|
|
// Allow single player to alter game state anywhere
|
|
if (network_get_mode() != NETWORK_MODE_NONE)
|
|
{
|
|
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.");
|
|
}
|
|
}
|
|
}
|
|
|
|
int32_t OpenRCT2::Scripting::GetTargetAPIVersion()
|
|
{
|
|
auto& scriptEngine = GetContext()->GetScriptEngine();
|
|
auto& execInfo = scriptEngine.GetExecInfo();
|
|
|
|
// Commands from the in-game console do not have a plug-in set
|
|
auto plugin = execInfo.GetCurrentPlugin();
|
|
if (plugin == nullptr)
|
|
{
|
|
// For in-game console, default to the current API version
|
|
return OPENRCT2_PLUGIN_API_VERSION;
|
|
}
|
|
|
|
return plugin->GetTargetAPIVersion();
|
|
}
|
|
|
|
duk_bool_t duk_exec_timeout_check(void*)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
#endif
|