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