diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts
index 4f962147a5..698fde90ec 100644
--- a/distribution/openrct2.d.ts
+++ b/distribution/openrct2.d.ts
@@ -15,6 +15,17 @@
// ///
//
+export type PluginType = "server" | "client" | "server_client";
+
+export interface PluginMetadata {
+ name: string;
+ version: string;
+ authors: string | string[];
+ type: PluginType;
+ minApiVersion: number;
+ main: () => void;
+}
+
export interface Console {
clear(): void;
log(message?: any, ...optionalParams: any[]): void;
diff --git a/src/openrct2/Game.cpp b/src/openrct2/Game.cpp
index c323435ec7..d19a9937db 100644
--- a/src/openrct2/Game.cpp
+++ b/src/openrct2/Game.cpp
@@ -46,6 +46,7 @@
#include "ride/TrackDesign.h"
#include "ride/Vehicle.h"
#include "scenario/Scenario.h"
+#include "scripting/ScriptEngine.h"
#include "title/TitleScreen.h"
#include "ui/UiContext.h"
#include "ui/WindowManager.h"
@@ -587,6 +588,13 @@ void game_load_init()
audio_stop_title_music();
gGameSpeed = 1;
+
+ GetContext()->GetScriptEngine().LoadPlugins();
+}
+
+void game_finish()
+{
+ GetContext()->GetScriptEngine().UnloadPlugins();
}
/**
@@ -801,6 +809,7 @@ static void game_load_or_quit_no_save_prompt_callback(int32_t result, const utf8
{
if (result == MODAL_RESULT_OK)
{
+ game_finish();
window_close_by_class(WC_EDITOR_OBJECT_SELECTION);
context_load_park_from_file(path);
}
@@ -843,10 +852,12 @@ void game_load_or_quit_no_save_prompt()
}
gGameSpeed = 1;
gFirstTimeSaving = true;
+ game_finish();
title_load();
break;
}
default:
+ game_finish();
openrct2_finish();
break;
}
diff --git a/src/openrct2/scripting/HookEngine.cpp b/src/openrct2/scripting/HookEngine.cpp
index ae93d31f78..d87c7d4669 100644
--- a/src/openrct2/scripting/HookEngine.cpp
+++ b/src/openrct2/scripting/HookEngine.cpp
@@ -78,6 +78,15 @@ void HookEngine::UnsubscribeAll(std::shared_ptr owner)
}
}
+void HookEngine::UnsubscribeAll()
+{
+ for (auto& hookList : _hookMap)
+ {
+ auto& hooks = hookList.Hooks;
+ hooks.clear();
+ }
+}
+
void HookEngine::Call(HOOK_TYPE type)
{
auto& hookList = GetHookList(type);
diff --git a/src/openrct2/scripting/HookEngine.h b/src/openrct2/scripting/HookEngine.h
index 9d476a10b0..7c417e64c5 100644
--- a/src/openrct2/scripting/HookEngine.h
+++ b/src/openrct2/scripting/HookEngine.h
@@ -79,6 +79,7 @@ namespace OpenRCT2::Scripting
uint32_t Subscribe(HOOK_TYPE type, std::shared_ptr owner, const DukValue& function);
void Unsubscribe(HOOK_TYPE type, uint32_t cookie);
void UnsubscribeAll(std::shared_ptr owner);
+ void UnsubscribeAll();
void Call(HOOK_TYPE type);
void Call(HOOK_TYPE type, const std::initializer_list>& args);
diff --git a/src/openrct2/scripting/Plugin.cpp b/src/openrct2/scripting/Plugin.cpp
index cdac2f3877..7c74139646 100644
--- a/src/openrct2/scripting/Plugin.cpp
+++ b/src/openrct2/scripting/Plugin.cpp
@@ -74,19 +74,40 @@ void Plugin::Start()
throw std::runtime_error("[" + _metadata.Name + "] " + val);
}
duk_pop(_context);
+
+ _hasStarted = true;
+}
+
+void Plugin::Stop()
+{
+ _hasStarted = false;
}
void Plugin::Update()
{
}
+static std::string TryGetString(const DukValue& value, const std::string& message)
+{
+ if (value.type() != DukValue::Type::STRING)
+ throw std::runtime_error(message);
+ return value.as_string();
+}
+
PluginMetadata Plugin::GetMetadata(const DukValue& dukMetadata)
{
PluginMetadata metadata;
if (dukMetadata.type() == DukValue::Type::OBJECT)
{
- metadata.Name = dukMetadata["name"].as_string();
- metadata.Version = dukMetadata["version"].as_string();
+ metadata.Name = TryGetString(dukMetadata["name"], "Plugin name not specified.");
+ metadata.Version = TryGetString(dukMetadata["version"], "Plugin version not specified.");
+ metadata.Type = ParsePluginType(TryGetString(dukMetadata["type"], "Plugin type not specified."));
+
+ auto dukMinApiVersion = dukMetadata["minApiVersion"];
+ if (dukMinApiVersion.type() == DukValue::Type::NUMBER)
+ {
+ metadata.MinApiVersion = dukMinApiVersion.as_int();
+ }
auto dukAuthors = dukMetadata["authors"];
dukAuthors.push();
@@ -97,7 +118,7 @@ PluginMetadata Plugin::GetMetadata(const DukValue& dukMetadata)
return v.as_string();
});
}
- else
+ else if (dukAuthors.type() == DukValue::Type::STRING)
{
metadata.Authors = { dukAuthors.as_string() };
}
@@ -105,3 +126,14 @@ PluginMetadata Plugin::GetMetadata(const DukValue& dukMetadata)
}
return metadata;
}
+
+PluginType Plugin::ParsePluginType(const std::string_view& type)
+{
+ if (type == "server")
+ return PluginType::Server;
+ if (type == "client")
+ return PluginType::Server;
+ if (type == "server_client")
+ return PluginType::Server;
+ throw std::invalid_argument("Unknown plugin type.");
+}
diff --git a/src/openrct2/scripting/Plugin.h b/src/openrct2/scripting/Plugin.h
index 4ae6b8a8d3..b005cba4b3 100644
--- a/src/openrct2/scripting/Plugin.h
+++ b/src/openrct2/scripting/Plugin.h
@@ -13,24 +13,48 @@
#include
#include
+#include
#include
namespace OpenRCT2::Scripting
{
+ enum class PluginType
+ {
+ /**
+ * Scripts that can run on any client with no impact on the game state.
+ */
+ Client,
+
+ /**
+ * Scripts that can run on servers with no impact on the game state and will not
+ * be uploaded to clients.
+ */
+ Server,
+
+ /**
+ * Scripts that can run on servers and will be uploaded to clients with ability to
+ * modify game state in certain contexts.
+ */
+ ServerClient,
+ };
+
struct PluginMetadata
{
std::string Name;
std::string Version;
std::vector Authors;
+ PluginType Type;
+ int32_t MinApiVersion{};
DukValue Main;
};
class Plugin
{
private:
- duk_context* _context;
+ duk_context* _context{};
std::string _path;
PluginMetadata _metadata;
+ bool _hasStarted{};
public:
std::string GetPath() const
@@ -38,6 +62,16 @@ namespace OpenRCT2::Scripting
return _path;
};
+ const PluginMetadata& GetMetadata() const
+ {
+ return _metadata;
+ }
+
+ bool HasStarted() const
+ {
+ return _hasStarted;
+ }
+
Plugin()
{
}
@@ -47,9 +81,11 @@ namespace OpenRCT2::Scripting
void Load();
void Start();
+ void Stop();
void Update();
private:
static PluginMetadata GetMetadata(const DukValue& dukMetadata);
+ static PluginType ParsePluginType(const std::string_view& type);
};
} // namespace OpenRCT2::Scripting
diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp
index 253338beef..8632d94028 100644
--- a/src/openrct2/scripting/ScriptEngine.cpp
+++ b/src/openrct2/scripting/ScriptEngine.cpp
@@ -32,6 +32,8 @@ using namespace OpenRCT2::Scripting;
static std::string Stringify(duk_context* ctx, duk_idx_t idx);
+static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 1;
+
DukContext::DukContext()
{
_context = duk_create_heap_default();
@@ -74,8 +76,9 @@ void ScriptEngine::Initialise()
dukglue_register_global(ctx, std::make_shared(ctx), "network");
dukglue_register_global(ctx, std::make_shared(), "park");
- LoadPlugins();
- StartPlugins();
+ _initialised = true;
+ _pluginsLoaded = false;
+ _pluginsStarted = false;
}
void ScriptEngine::LoadPlugins()
@@ -93,7 +96,17 @@ void ScriptEngine::LoadPlugins()
auto plugin = std::make_shared(_context, path);
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin);
plugin->Load();
- _plugins.push_back(std::move(plugin));
+
+ 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)
{
@@ -115,6 +128,8 @@ void ScriptEngine::LoadPlugins()
{
std::printf("Unable to enable hot reloading of plugins: %s\n", e.what());
}
+
+ _pluginsLoaded = true;
}
bool ScriptEngine::ShouldLoadScript(const std::string& path)
@@ -142,6 +157,7 @@ void ScriptEngine::AutoReloadPlugins()
ScriptExecutionInfo::PluginScope scope(_execInfo, plugin);
plugin->Load();
+ LogPluginInfo(plugin, "Reloaded");
plugin->Start();
}
catch (const std::exception& e)
@@ -154,20 +170,57 @@ void ScriptEngine::AutoReloadPlugins()
}
}
+void ScriptEngine::UnloadPlugins()
+{
+ StopPlugins();
+ for (auto& plugin : _plugins)
+ {
+ LogPluginInfo(plugin, "Unloaded");
+ }
+ _plugins.clear();
+ _pluginsLoaded = false;
+ _pluginsStarted = false;
+}
+
void ScriptEngine::StartPlugins()
{
for (auto& plugin : _plugins)
{
- ScriptExecutionInfo::PluginScope scope(_execInfo, plugin);
- try
+ if (!plugin->HasStarted())
{
- plugin->Start();
- }
- catch (const std::exception& e)
- {
- _console.WriteLineError(e.what());
+ ScriptExecutionInfo::PluginScope scope(_execInfo, plugin);
+ try
+ {
+ plugin->Start();
+ }
+ catch (const std::exception& e)
+ {
+ _console.WriteLineError(e.what());
+ }
}
}
+ _pluginsStarted = true;
+}
+
+void ScriptEngine::StopPlugins()
+{
+ _hookEngine.UnsubscribeAll();
+ for (auto& plugin : _plugins)
+ {
+ if (plugin->HasStarted())
+ {
+ ScriptExecutionInfo::PluginScope scope(_execInfo, plugin);
+ try
+ {
+ plugin->Stop();
+ }
+ catch (const std::exception& e)
+ {
+ _console.WriteLineError(e.what());
+ }
+ }
+ }
+ _pluginsStarted = false;
}
void ScriptEngine::Update()
@@ -175,8 +228,30 @@ void ScriptEngine::Update()
if (!_initialised)
{
Initialise();
- _initialised = true;
}
+
+ 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());
@@ -197,13 +272,6 @@ void ScriptEngine::Update()
// Signal the promise so caller can continue
promise.set_value();
}
-
- auto tick = Platform::GetTicks();
- if (tick - _lastHotReloadCheckTick > 1000)
- {
- AutoReloadPlugins();
- _lastHotReloadCheckTick = tick;
- }
}
std::future ScriptEngine::Eval(const std::string& s)
@@ -214,6 +282,12 @@ std::future ScriptEngine::Eval(const std::string& s)
return future;
}
+void ScriptEngine::LogPluginInfo(const std::shared_ptr& plugin, const std::string_view& message)
+{
+ const auto& pluginName = plugin->GetMetadata().Name;
+ _console.WriteLine("[" + pluginName + "] " + std::string(message));
+}
+
static std::string Stringify(duk_context* ctx, duk_idx_t idx)
{
auto type = duk_get_type(ctx, idx);
diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h
index 90898ea19a..398e63bf78 100644
--- a/src/openrct2/scripting/ScriptEngine.h
+++ b/src/openrct2/scripting/ScriptEngine.h
@@ -74,7 +74,7 @@ namespace OpenRCT2::Scripting
public:
DukContext();
DukContext(DukContext&) = delete;
- DukContext(DukContext&& src)
+ DukContext(DukContext&& src) noexcept
: _context(std::move(src._context))
{
}
@@ -93,6 +93,8 @@ namespace OpenRCT2::Scripting
IPlatformEnvironment& _env;
DukContext _context;
bool _initialised{};
+ bool _pluginsLoaded{};
+ bool _pluginsStarted{};
std::queue, std::string>> _evalQueue;
std::vector> _plugins;
uint32_t _lastHotReloadCheckTick{};
@@ -120,14 +122,19 @@ namespace OpenRCT2::Scripting
return _execInfo;
}
+ void LoadPlugins();
+ void UnloadPlugins();
void Update();
std::future Eval(const std::string& s);
+ void LogPluginInfo(const std::shared_ptr& plugin, const std::string_view& message);
+
private:
void Initialise();
- void LoadPlugins();
void StartPlugins();
+ void StopPlugins();
bool ShouldLoadScript(const std::string& path);
void AutoReloadPlugins();
+ void ProcessREPL();
};
} // namespace OpenRCT2::Scripting