From 6fea0b5025db91197cd195d2716ec343db4dff1e Mon Sep 17 00:00:00 2001 From: Ted John Date: Tue, 2 Mar 2021 00:17:13 +0000 Subject: [PATCH 01/13] Start work on intransient plugins --- distribution/openrct2.d.ts | 2 +- src/openrct2/scripting/Plugin.cpp | 21 ++- src/openrct2/scripting/Plugin.h | 18 +- src/openrct2/scripting/ScriptEngine.cpp | 210 +++++++++++++++++++++--- src/openrct2/scripting/ScriptEngine.h | 11 +- 5 files changed, 235 insertions(+), 27 deletions(-) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 3d64a8306f..5205d4dca4 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -15,7 +15,7 @@ // /// // -export type PluginType = "local" | "remote"; +export type PluginType = "local" | "remote" | intransient; declare global { /** diff --git a/src/openrct2/scripting/Plugin.cpp b/src/openrct2/scripting/Plugin.cpp index 1ac86345e8..89afb8f446 100644 --- a/src/openrct2/scripting/Plugin.cpp +++ b/src/openrct2/scripting/Plugin.cpp @@ -23,7 +23,7 @@ using namespace OpenRCT2::Scripting; -Plugin::Plugin(duk_context* context, const std::string& path) +Plugin::Plugin(duk_context* context, std::string_view path) : _context(context) , _path(path) { @@ -73,10 +73,16 @@ void Plugin::Load() } _metadata = GetMetadata(DukValue::take_from_stack(_context)); + _hasLoaded = true; } void Plugin::Start() { + if (!_hasLoaded) + { + throw std::runtime_error("Plugin has not been loaded."); + } + const auto& mainFunc = _metadata.Main; if (mainFunc.context() == nullptr) { @@ -115,6 +121,12 @@ void Plugin::ThrowIfStopping() const } } +void Plugin::Unload() +{ + _metadata.Main = {}; + _hasLoaded = false; +} + void Plugin::LoadCodeFromFile() { _code = File::ReadAllText(_path); @@ -174,6 +186,8 @@ PluginType Plugin::ParsePluginType(std::string_view type) return PluginType::Local; if (type == "remote") return PluginType::Remote; + if (type == "intransient") + return PluginType::Intransient; throw std::invalid_argument("Unknown plugin type."); } @@ -192,4 +206,9 @@ int32_t Plugin::GetTargetAPIVersion() const return 33; } +bool Plugin::IsTransient() const +{ + return _metadata.Type != PluginType::Intransient; +} + #endif diff --git a/src/openrct2/scripting/Plugin.h b/src/openrct2/scripting/Plugin.h index 6bda824c37..5e150b821c 100644 --- a/src/openrct2/scripting/Plugin.h +++ b/src/openrct2/scripting/Plugin.h @@ -33,6 +33,11 @@ namespace OpenRCT2::Scripting * modify game state in certain contexts. */ Remote, + + /** + * Scripts that run when the game starts and never unload. + */ + Intransient, }; struct PluginMetadata @@ -53,11 +58,12 @@ namespace OpenRCT2::Scripting std::string _path; PluginMetadata _metadata{}; std::string _code; + bool _hasLoaded{}; bool _hasStarted{}; bool _isStopping{}; public: - std::string GetPath() const + std::string_view GetPath() const { return _path; }; @@ -87,10 +93,15 @@ namespace OpenRCT2::Scripting return _isStopping; } + bool IsLoaded() const + { + return _hasLoaded; + } + int32_t GetTargetAPIVersion() const; Plugin() = default; - Plugin(duk_context* context, const std::string& path); + Plugin(duk_context* context, std::string_view path); Plugin(const Plugin&) = delete; Plugin(Plugin&&) = delete; @@ -101,6 +112,9 @@ namespace OpenRCT2::Scripting void StopEnd(); void ThrowIfStopping() const; + void Unload(); + + bool IsTransient() const; private: void LoadCodeFromFile(); diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 123dd2b89d..6a039c66c9 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -443,6 +443,149 @@ void ScriptEngine::Initialise() ClearParkStorage(); } +void ScriptEngine::RefreshPlugins() +{ + if (!_initialised) + { + Initialise(); + } + + // Get a list of removed and added plugin files + auto pluginFiles = GetPluginFiles(); + std::vector plugins; + std::vector removedPlugins; + std::vector addedPlugins; + for (const auto& plugin : _plugins) + { + plugins.push_back(std::string(plugin->GetPath())); + } + 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 (!_hotReloading && gConfigPlugin.enable_hot_reloading && network_get_mode() == NETWORK_MODE_NONE) + { + SetupHotReloading(); + } + + // Start any new intransient plugins + StartIntransientPlugins(); +} + +std::vector ScriptEngine::GetPluginFiles() const +{ + // Scan for .js files in plugin directory + std::vector pluginFiles; + auto base = _env.GetDirectoryPath(DIRBASE::USER, DIRID::PLUGIN); + if (Path::DirectoryExists(base)) + { + auto pattern = Path::Combine(base, "*.js"); + auto scanner = std::unique_ptr(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) { + 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(_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() +{ + for (auto& plugin : _plugins) + { + if (!plugin->HasStarted() && !plugin->IsTransient()) + { + LoadPlugin(plugin); + StartPlugin(plugin); + } + } +} + +void ScriptEngine::StopUnloadRegisterAllPlugins() +{ + std::vector pluginPaths; + for (auto& plugin : _plugins) + { + pluginPaths.push_back(std::string(plugin->GetPath())); + StopPlugin(plugin); + } + for (auto& plugin : _plugins) + { + UnloadPlugin(plugin); + } + for (auto& pluginPath : pluginPaths) + { + UnregisterPlugin(pluginPath); + } +} + void ScriptEngine::LoadPlugins() { if (!_initialised) @@ -487,18 +630,19 @@ void ScriptEngine::LoadPlugin(std::shared_ptr& plugin) { try { - ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false); - plugin->Load(); - - auto metadata = plugin->GetMetadata(); - if (metadata.MinApiVersion <= OPENRCT2_PLUGIN_API_VERSION) + if (!plugin->IsLoaded()) { - LogPluginInfo(plugin, "Loaded"); - _plugins.push_back(std::move(plugin)); - } - else - { - LogPluginInfo(plugin, "Requires newer API version: v" + std::to_string(metadata.MinApiVersion)); + 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) @@ -507,6 +651,29 @@ void ScriptEngine::LoadPlugin(std::shared_ptr& plugin) } } +void ScriptEngine::UnloadPlugin(std::shared_ptr& plugin) +{ + plugin->Unload(); + LogPluginInfo(plugin, "Unloaded"); +} + +void ScriptEngine::StartPlugin(std::shared_ptr 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) { if (plugin->HasStarted()) @@ -526,22 +693,20 @@ void ScriptEngine::StopPlugin(std::shared_ptr plugin) } } -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(base); - _pluginFileWatcher->OnFileChanged = [this](const std::string& path) { - std::lock_guard guard(_changedPluginFilesMutex); - _changedPluginFiles.emplace(path); - }; + if (Path::DirectoryExists(base)) + { + _pluginFileWatcher = std::make_unique(base); + _pluginFileWatcher->OnFileChanged = [this](const std::string& path) { + std::lock_guard guard(_changedPluginFilesMutex); + _changedPluginFiles.emplace(path); + }; + _hotReloading = true; + } } catch (const std::exception& e) { @@ -652,6 +817,7 @@ void ScriptEngine::Tick() if (!_initialised) { Initialise(); + RefreshPlugins(); } if (_pluginsLoaded) diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 5478e43895..10d3253f74 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -145,6 +145,7 @@ namespace OpenRCT2::Scripting IPlatformEnvironment& _env; DukContext _context; bool _initialised{}; + bool _hotReloading{}; bool _pluginsLoaded{}; bool _pluginsStarted{}; std::queue, std::string>> _evalQueue; @@ -247,12 +248,20 @@ namespace OpenRCT2::Scripting private: void Initialise(); + void RefreshPlugins(); + std::vector GetPluginFiles() const; + void UnregisterPlugin(std::string_view path); + void RegisterPlugin(std::string_view path); + void StartIntransientPlugins(); void StartPlugins(); void StopPlugins(); void LoadPlugin(const std::string& path); void LoadPlugin(std::shared_ptr& plugin); + void UnloadPlugin(std::shared_ptr& plugin); + void StartPlugin(std::shared_ptr plugin); void StopPlugin(std::shared_ptr plugin); - bool ShouldLoadScript(const std::string& path); + void StopUnloadRegisterAllPlugins(); + static bool ShouldLoadScript(std::string_view path); bool ShouldStartPlugin(const std::shared_ptr& plugin); void SetupHotReloading(); void AutoReloadPlugins(); From dbf83f018c2233e0fa9aa0a81ed6db0c63f19d37 Mon Sep 17 00:00:00 2001 From: Ted John Date: Tue, 2 Mar 2021 22:55:41 +0000 Subject: [PATCH 02/13] Implement lifetime logic of transient plugins --- src/openrct2/Game.cpp | 4 +- src/openrct2/scripting/ScriptEngine.cpp | 157 ++++++++++-------------- src/openrct2/scripting/ScriptEngine.h | 12 +- 3 files changed, 76 insertions(+), 97 deletions(-) diff --git a/src/openrct2/Game.cpp b/src/openrct2/Game.cpp index b0f0739be9..433a10f67a 100644 --- a/src/openrct2/Game.cpp +++ b/src/openrct2/Game.cpp @@ -502,14 +502,14 @@ void game_load_init() void game_load_scripts() { #ifdef ENABLE_SCRIPTING - GetContext()->GetScriptEngine().LoadPlugins(); + GetContext()->GetScriptEngine().LoadTransientPlugins(); #endif } void game_unload_scripts() { #ifdef ENABLE_SCRIPTING - GetContext()->GetScriptEngine().UnloadPlugins(); + GetContext()->GetScriptEngine().UnloadTransientPlugins(); #endif } diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 6a039c66c9..a49ab36964 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -436,10 +436,10 @@ void ScriptEngine::Initialise() dukglue_register_global(ctx, std::make_shared(), "scenario"); _initialised = true; - _pluginsLoaded = false; - _pluginsStarted = false; + _transientPluginsEnabled = false; + _transientPluginsStarted = false; - InitSharedStorage(); + LoadSharedStorage(); ClearParkStorage(); } @@ -477,7 +477,7 @@ void ScriptEngine::RefreshPlugins() } // Turn on hot reload if not already enabled - if (!_hotReloading && gConfigPlugin.enable_hot_reloading && network_get_mode() == NETWORK_MODE_NONE) + if (!_hotReloadingInitialised && gConfigPlugin.enable_hot_reloading && network_get_mode() == NETWORK_MODE_NONE) { SetupHotReloading(); } @@ -493,8 +493,8 @@ std::vector ScriptEngine::GetPluginFiles() const auto base = _env.GetDirectoryPath(DIRBASE::USER, DIRID::PLUGIN); if (Path::DirectoryExists(base)) { - auto pattern = Path::Combine(base, "*.js"); - auto scanner = std::unique_ptr(Path::ScanDirectory(pattern, true)); + auto pattern = Path::Combine(base, u8"*.js"); + auto scanner = Path::ScanDirectory(pattern, true); while (scanner->Next()) { auto path = std::string(scanner->GetPath()); @@ -558,6 +558,8 @@ void ScriptEngine::RegisterPlugin(std::string_view path) void ScriptEngine::StartIntransientPlugins() { + LoadSharedStorage(); + for (auto& plugin : _plugins) { if (!plugin->HasStarted() && !plugin->IsTransient()) @@ -586,38 +588,9 @@ void ScriptEngine::StopUnloadRegisterAllPlugins() } } -void ScriptEngine::LoadPlugins() +void ScriptEngine::LoadTransientPlugins() { - if (!_initialised) - { - Initialise(); - } - if (_pluginsLoaded) - { - UnloadPlugins(); - } - - 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)) - { - LoadPlugin(path); - } - } - - if (gConfigPlugin.enable_hot_reloading && network_get_mode() == NETWORK_MODE_NONE) - { - SetupHotReloading(); - } - } - _pluginsLoaded = true; - _pluginsStarted = false; + _transientPluginsEnabled = true; } void ScriptEngine::LoadPlugin(const std::string& path) @@ -705,7 +678,7 @@ void ScriptEngine::SetupHotReloading() std::lock_guard guard(_changedPluginFilesMutex); _changedPluginFiles.emplace(path); }; - _hotReloading = true; + _hotReloadingInitialised = true; } } catch (const std::exception& e) @@ -714,6 +687,19 @@ void ScriptEngine::SetupHotReloading() } } +void ScriptEngine::DoAutoReloadPluginCheck() +{ + if (_hotReloadingInitialised) + { + auto tick = Platform::GetTicks(); + if (tick - _lastHotReloadCheckTick > 1000) + { + AutoReloadPlugins(); + _lastHotReloadCheckTick = tick; + } + } +} + void ScriptEngine::AutoReloadPlugins() { if (_changedPluginFiles.size() > 0) @@ -746,39 +732,54 @@ void ScriptEngine::AutoReloadPlugins() } } -void ScriptEngine::UnloadPlugins() +void ScriptEngine::UnloadTransientPlugins() { - StopPlugins(); + // Stop them all first for (auto& plugin : _plugins) { - LogPluginInfo(plugin, "Unloaded"); - } - _plugins.clear(); - _pluginsLoaded = false; - _pluginsStarted = false; -} - -void ScriptEngine::StartPlugins() -{ - LoadSharedStorage(); - - for (auto& plugin : _plugins) - { - if (!plugin->HasStarted() && ShouldStartPlugin(plugin)) + if (plugin->IsTransient()) { - ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false); - try + if (plugin->HasStarted()) { - LogPluginInfo(plugin, "Started"); - plugin->Start(); - } - catch (const std::exception& e) - { - _console.WriteLineError(e.what()); + StopPlugin(plugin); + LogPluginInfo(plugin, "Stopped"); } } } - _pluginsStarted = true; + + // Now unload them + for (auto& plugin : _plugins) + { + 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) @@ -797,19 +798,6 @@ bool ScriptEngine::ShouldStartPlugin(const std::shared_ptr& plugin) return true; } -void ScriptEngine::StopPlugins() -{ - for (auto& plugin : _plugins) - { - if (plugin->HasStarted()) - { - StopPlugin(plugin); - LogPluginInfo(plugin, "Stopped"); - } - } - _pluginsStarted = false; -} - void ScriptEngine::Tick() { PROFILED_FUNCTION(); @@ -820,26 +808,15 @@ void ScriptEngine::Tick() RefreshPlugins(); } - if (_pluginsLoaded) + if (_transientPluginsEnabled && !_transientPluginsStarted) { - if (!_pluginsStarted) - { - StartPlugins(); - } - else - { - auto tick = Platform::GetTicks(); - if (tick - _lastHotReloadCheckTick > 1000) - { - AutoReloadPlugins(); - _lastHotReloadCheckTick = tick; - } - } + StartTransientPlugins(); } UpdateIntervals(); UpdateSockets(); ProcessREPL(); + DoAutoReloadPluginCheck(); } void ScriptEngine::ProcessREPL() diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 10d3253f74..17020fc7cc 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -145,9 +145,9 @@ namespace OpenRCT2::Scripting IPlatformEnvironment& _env; DukContext _context; bool _initialised{}; - bool _hotReloading{}; - bool _pluginsLoaded{}; - bool _pluginsStarted{}; + bool _hotReloadingInitialised{}; + bool _transientPluginsEnabled{}; + bool _transientPluginsStarted{}; std::queue, std::string>> _evalQueue; std::vector> _plugins; uint32_t _lastHotReloadCheckTick{}; @@ -212,6 +212,8 @@ namespace OpenRCT2::Scripting void LoadPlugins(); void UnloadPlugins(); + void LoadTransientPlugins(); + void UnloadTransientPlugins(); void Tick(); std::future Eval(const std::string& s); DukValue ExecutePluginCall( @@ -253,8 +255,7 @@ namespace OpenRCT2::Scripting void UnregisterPlugin(std::string_view path); void RegisterPlugin(std::string_view path); void StartIntransientPlugins(); - void StartPlugins(); - void StopPlugins(); + void StartTransientPlugins(); void LoadPlugin(const std::string& path); void LoadPlugin(std::shared_ptr& plugin); void UnloadPlugin(std::shared_ptr& plugin); @@ -264,6 +265,7 @@ namespace OpenRCT2::Scripting static bool ShouldLoadScript(std::string_view path); bool ShouldStartPlugin(const std::shared_ptr& plugin); void SetupHotReloading(); + void DoAutoReloadPluginCheck(); void AutoReloadPlugins(); void ProcessREPL(); void RemoveCustomGameActions(const std::shared_ptr& plugin); From 1128f76a9722d8e38aa1026023ee79520748d03b Mon Sep 17 00:00:00 2001 From: Ted John Date: Sun, 20 Feb 2022 00:23:15 +0000 Subject: [PATCH 03/13] Improve and fix intransient plugin branch --- distribution/openrct2.d.ts | 2 +- src/openrct2/scripting/Plugin.h | 3 +- src/openrct2/scripting/ScriptEngine.cpp | 37 +++++++++++++++---------- src/openrct2/scripting/ScriptEngine.h | 1 + 4 files changed, 27 insertions(+), 16 deletions(-) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 5205d4dca4..5a62087759 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -15,7 +15,7 @@ // /// // -export type PluginType = "local" | "remote" | intransient; +export type PluginType = "local" | "remote" | "intransient"; declare global { /** diff --git a/src/openrct2/scripting/Plugin.h b/src/openrct2/scripting/Plugin.h index 5e150b821c..6af3c15b8f 100644 --- a/src/openrct2/scripting/Plugin.h +++ b/src/openrct2/scripting/Plugin.h @@ -35,7 +35,8 @@ namespace OpenRCT2::Scripting Remote, /** - * Scripts that run when the game starts and never unload. + * Scripts that run when the game starts and only unload explicitly rather than when the + * park changes. */ Intransient, }; diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index a49ab36964..7cf2fa98b4 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -626,8 +626,11 @@ void ScriptEngine::LoadPlugin(std::shared_ptr& plugin) void ScriptEngine::UnloadPlugin(std::shared_ptr& plugin) { - plugin->Unload(); - LogPluginInfo(plugin, "Unloaded"); + if (plugin->IsLoaded()) + { + plugin->Unload(); + LogPluginInfo(plugin, "Unloaded"); + } } void ScriptEngine::StartPlugin(std::shared_ptr plugin) @@ -663,9 +666,21 @@ void ScriptEngine::StopPlugin(std::shared_ptr plugin) _hookEngine.UnsubscribeAll(plugin); plugin->StopEnd(); + LogPluginInfo(plugin, "Stopped"); } } +void ScriptEngine::ReloadPlugin(std::shared_ptr plugin) +{ + StopPlugin(plugin); + { + ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false); + plugin->Load(); + LogPluginInfo(plugin, "Reloaded"); + } + StartPlugin(plugin); +} + void ScriptEngine::SetupHotReloading() { try @@ -715,12 +730,7 @@ void ScriptEngine::AutoReloadPlugins() auto& plugin = *findResult; try { - StopPlugin(plugin); - - ScriptExecutionInfo::PluginScope scope(_execInfo, plugin, false); - plugin->Load(); - LogPluginInfo(plugin, "Reloaded"); - plugin->Start(); + ReloadPlugin(plugin); } catch (const std::exception& e) { @@ -739,18 +749,17 @@ void ScriptEngine::UnloadTransientPlugins() { if (plugin->IsTransient()) { - if (plugin->HasStarted()) - { - StopPlugin(plugin); - LogPluginInfo(plugin, "Stopped"); - } + StopPlugin(plugin); } } // Now unload them for (auto& plugin : _plugins) { - UnloadPlugin(plugin); + if (plugin->IsTransient()) + { + UnloadPlugin(plugin); + } } _transientPluginsEnabled = false; diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 17020fc7cc..01633ca8f7 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -261,6 +261,7 @@ namespace OpenRCT2::Scripting void UnloadPlugin(std::shared_ptr& plugin); void StartPlugin(std::shared_ptr plugin); void StopPlugin(std::shared_ptr plugin); + void ReloadPlugin(std::shared_ptr plugin); void StopUnloadRegisterAllPlugins(); static bool ShouldLoadScript(std::string_view path); bool ShouldStartPlugin(const std::shared_ptr& plugin); From 84fdd44e6a5eaf705a170f7c7bae9332ad7ff6a9 Mon Sep 17 00:00:00 2001 From: Ted John Date: Sun, 20 Feb 2022 02:05:24 +0000 Subject: [PATCH 04/13] Add map changed hook --- distribution/openrct2.d.ts | 7 ++++++- src/openrct2-ui/title/TitleSequencePlayer.cpp | 12 ++++++++++-- src/openrct2-ui/windows/TitleMenu.cpp | 1 + src/openrct2-ui/windows/TopToolbar.cpp | 1 + src/openrct2/Context.cpp | 1 + src/openrct2/Game.cpp | 12 ++++++++++++ src/openrct2/Game.h | 1 + src/openrct2/network/NetworkBase.cpp | 2 ++ src/openrct2/scripting/HookEngine.cpp | 10 ++++++++++ src/openrct2/scripting/HookEngine.h | 2 ++ src/openrct2/scripting/bindings/game/ScContext.hpp | 5 +++++ src/openrct2/title/TitleScreen.cpp | 1 + 12 files changed, 52 insertions(+), 3 deletions(-) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index 5a62087759..d935a75867 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -287,6 +287,11 @@ declare global { subscribe(hook: "vehicle.crash", callback: (e: VehicleCrashArgs) => void): IDisposable; subscribe(hook: "map.save", callback: () => void): IDisposable; + /** + * Can only be used in intransient plugins. + */ + subscribe(hook: "map.changed", callback: () => void): IDisposable; + /** * Registers a function to be called every so often in realtime, specified by the given delay. * @param callback The function to call every time the delay has elapsed. @@ -387,7 +392,7 @@ declare global { "interval.tick" | "interval.day" | "network.chat" | "network.action" | "network.join" | "network.leave" | "ride.ratings.calculate" | "action.location" | "vehicle.crash" | - "map.save"; + "map.changed" | "map.save"; type ExpenditureType = "ride_construction" | diff --git a/src/openrct2-ui/title/TitleSequencePlayer.cpp b/src/openrct2-ui/title/TitleSequencePlayer.cpp index 4426219a59..8050409ade 100644 --- a/src/openrct2-ui/title/TitleSequencePlayer.cpp +++ b/src/openrct2-ui/title/TitleSequencePlayer.cpp @@ -288,7 +288,11 @@ private: { loadSuccess = LoadParkFromStream(parkHandle->Stream.get(), parkHandle->HintPath); } - if (!loadSuccess) + if (loadSuccess) + { + game_notify_map_changed(); + } + else { if (_sequence->Saves.size() > saveIndex) { @@ -307,7 +311,11 @@ private: { loadSuccess = LoadParkFromFile(scenario->path); } - if (!loadSuccess) + if (loadSuccess) + { + game_notify_map_changed(); + } + else { Console::Error::WriteLine("Failed to load: \"%s\" for the title sequence.", command.Scenario); return false; diff --git a/src/openrct2-ui/windows/TitleMenu.cpp b/src/openrct2-ui/windows/TitleMenu.cpp index 25684f0af8..8698db5f15 100644 --- a/src/openrct2-ui/windows/TitleMenu.cpp +++ b/src/openrct2-ui/windows/TitleMenu.cpp @@ -108,6 +108,7 @@ static void WindowTitleMenuScenarioselectCallback(const utf8* path) { OpenRCT2::GetContext()->LoadParkFromFile(path, false, true); game_load_scripts(); + game_notify_map_changed(); } static void WindowTitleMenuMouseup(rct_window* w, rct_widgetindex widgetIndex) diff --git a/src/openrct2-ui/windows/TopToolbar.cpp b/src/openrct2-ui/windows/TopToolbar.cpp index f176e0e469..02bdfe8490 100644 --- a/src/openrct2-ui/windows/TopToolbar.cpp +++ b/src/openrct2-ui/windows/TopToolbar.cpp @@ -537,6 +537,7 @@ static void WindowTopToolbarScenarioselectCallback(const utf8* path) window_close_by_class(WC_EDITOR_OBJECT_SELECTION); GetContext()->LoadParkFromFile(path, false, true); game_load_scripts(); + game_notify_map_changed(); } /** diff --git a/src/openrct2/Context.cpp b/src/openrct2/Context.cpp index 4ae0c71197..49be7bf598 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -918,6 +918,7 @@ namespace OpenRCT2 #endif // DISABLE_NETWORK { game_load_scripts(); + game_notify_map_changed(); } break; } diff --git a/src/openrct2/Game.cpp b/src/openrct2/Game.cpp index 433a10f67a..85bfb40460 100644 --- a/src/openrct2/Game.cpp +++ b/src/openrct2/Game.cpp @@ -513,6 +513,17 @@ void game_unload_scripts() #endif } +void game_notify_map_changed() +{ +#ifdef ENABLE_SCRIPTING + using namespace OpenRCT2::Scripting; + + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto& hookEngine = scriptEngine.GetHookEngine(); + hookEngine.Call(HOOK_TYPE::MAP_CHANGE, false); +#endif +} + /** * * rct2: 0x0069E9A7 @@ -696,6 +707,7 @@ static void game_load_or_quit_no_save_prompt_callback(int32_t result, const utf8 window_close_by_class(WC_EDITOR_OBJECT_SELECTION); context_load_park_from_file(path); game_load_scripts(); + game_notify_map_changed(); } } diff --git a/src/openrct2/Game.h b/src/openrct2/Game.h index c76a99a936..1e8465e114 100644 --- a/src/openrct2/Game.h +++ b/src/openrct2/Game.h @@ -160,6 +160,7 @@ void load_from_sv6(const char* path); void game_load_init(); void game_load_scripts(); void game_unload_scripts(); +void game_notify_map_changed(); void pause_toggle(); bool game_is_paused(); bool game_is_not_paused(); diff --git a/src/openrct2/network/NetworkBase.cpp b/src/openrct2/network/NetworkBase.cpp index b547149912..026cfc27c9 100644 --- a/src/openrct2/network/NetworkBase.cpp +++ b/src/openrct2/network/NetworkBase.cpp @@ -403,6 +403,7 @@ bool NetworkBase::BeginServer(uint16_t port, const std::string& address) _advertiser = CreateServerAdvertiser(listening_port); game_load_scripts(); + game_notify_map_changed(); return true; } @@ -2688,6 +2689,7 @@ void NetworkBase::Client_Handle_MAP([[maybe_unused]] NetworkConnection& connecti { game_load_init(); game_load_scripts(); + game_notify_map_changed(); _serverState.tick = gCurrentTicks; // window_network_status_open("Loaded new map from network"); _serverState.state = NetworkServerState::Ok; diff --git a/src/openrct2/scripting/HookEngine.cpp b/src/openrct2/scripting/HookEngine.cpp index 33525160d9..a1f9d2302d 100644 --- a/src/openrct2/scripting/HookEngine.cpp +++ b/src/openrct2/scripting/HookEngine.cpp @@ -31,6 +31,7 @@ static const EnumMap HooksLookupTable({ { "action.location", HOOK_TYPE::ACTION_LOCATION }, { "guest.generation", HOOK_TYPE::GUEST_GENERATION }, { "vehicle.crash", HOOK_TYPE::VEHICLE_CRASH }, + { "map.change", HOOK_TYPE::MAP_CHANGE }, { "map.save", HOOK_TYPE::MAP_SAVE }, }); @@ -97,6 +98,15 @@ bool HookEngine::HasSubscriptions(HOOK_TYPE type) const return !hookList.Hooks.empty(); } +bool HookEngine::IsValidHookForPlugin(HOOK_TYPE type, Plugin& plugin) const +{ + if (type == HOOK_TYPE::MAP_CHANGE && plugin.GetMetadata().Type != PluginType::Intransient) + { + return false; + } + return true; +} + void HookEngine::Call(HOOK_TYPE type, bool isGameStateMutable) { auto& hookList = GetHookList(type); diff --git a/src/openrct2/scripting/HookEngine.h b/src/openrct2/scripting/HookEngine.h index dde0f4de1b..832388ace2 100644 --- a/src/openrct2/scripting/HookEngine.h +++ b/src/openrct2/scripting/HookEngine.h @@ -40,6 +40,7 @@ namespace OpenRCT2::Scripting ACTION_LOCATION, GUEST_GENERATION, VEHICLE_CRASH, + MAP_CHANGE, MAP_SAVE, COUNT, UNDEFINED = -1, @@ -87,6 +88,7 @@ namespace OpenRCT2::Scripting void UnsubscribeAll(std::shared_ptr owner); void UnsubscribeAll(); bool HasSubscriptions(HOOK_TYPE type) const; + bool IsValidHookForPlugin(HOOK_TYPE type, Plugin& plugin) const; void Call(HOOK_TYPE type, bool isGameStateMutable); void Call(HOOK_TYPE type, const DukValue& arg, bool isGameStateMutable); void Call( diff --git a/src/openrct2/scripting/bindings/game/ScContext.hpp b/src/openrct2/scripting/bindings/game/ScContext.hpp index 5b5592a848..2dc64f7a0c 100644 --- a/src/openrct2/scripting/bindings/game/ScContext.hpp +++ b/src/openrct2/scripting/bindings/game/ScContext.hpp @@ -278,6 +278,11 @@ namespace OpenRCT2::Scripting duk_error(ctx, DUK_ERR_ERROR, "Not in a plugin context"); } + if (!_hookEngine.IsValidHookForPlugin(hookType, *owner)) + { + duk_error(ctx, DUK_ERR_ERROR, "Hook type not available for this plugin type."); + } + auto cookie = _hookEngine.Subscribe(hookType, owner, callback); return std::make_shared([this, hookType, cookie]() { _hookEngine.Unsubscribe(hookType, cookie); }); } diff --git a/src/openrct2/title/TitleScreen.cpp b/src/openrct2/title/TitleScreen.cpp index 7fffc73097..7131969c9e 100644 --- a/src/openrct2/title/TitleScreen.cpp +++ b/src/openrct2/title/TitleScreen.cpp @@ -336,6 +336,7 @@ bool TitleScreen::TryLoadSequence(bool loadPreview) if (!loadPreview) { GetContext()->GetGameState()->InitAll(DEFAULT_MAP_SIZE); + game_notify_map_changed(); } return false; } From 13d261d115929d4dc1e26a1cdf269e1b3683f643 Mon Sep 17 00:00:00 2001 From: Ted John Date: Sun, 20 Feb 2022 18:25:54 +0000 Subject: [PATCH 05/13] Add API for getting the current game mode --- distribution/openrct2.d.ts | 17 +++++++++++++++-- .../scripting/bindings/game/ScContext.hpp | 15 +++++++++++++++ 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index d935a75867..a372e15268 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -173,7 +173,7 @@ declare global { /** * The user's current configuration. */ - configuration: Configuration; + readonly configuration: Configuration; /** * Shared generic storage for all plugins. Data is persistent across instances @@ -183,7 +183,7 @@ declare global { * the `set` method, do not rely on the file being saved by modifying your own * objects. Functions and other internal structures will not be persisted. */ - sharedStorage: Configuration; + readonly sharedStorage: Configuration; /** * Gets the storage for the current plugin if no name is specified. @@ -199,6 +199,12 @@ declare global { */ getParkStorage(pluginName?: string): Configuration; + /** + * The current mode / screen the game is in. Can be used for example to check + * whether the game is currently on the title screen or in the scenario editor. + */ + readonly mode: GameMode; + /** * Render the current state of the map and save to disc. * Useful for server administration and timelapse creation. @@ -370,6 +376,13 @@ declare global { transparent?: boolean; } + type GameMode = + "normal" | + "title" | + "scenario_editor" | + "track_designer" | + "track_manager"; + type ObjectType = "ride" | "small_scenery" | diff --git a/src/openrct2/scripting/bindings/game/ScContext.hpp b/src/openrct2/scripting/bindings/game/ScContext.hpp index 2dc64f7a0c..e2d960ac12 100644 --- a/src/openrct2/scripting/bindings/game/ScContext.hpp +++ b/src/openrct2/scripting/bindings/game/ScContext.hpp @@ -11,6 +11,7 @@ #ifdef ENABLE_SCRIPTING +# include "../../../OpenRCT2.h" # include "../../../actions/GameAction.h" # include "../../../interface/Screenshot.h" # include "../../../localisation/Formatting.h" @@ -110,6 +111,19 @@ namespace OpenRCT2::Scripting return result; } + std::string mode_get() + { + if (gScreenFlags & SCREEN_FLAGS_TITLE_DEMO) + return "title"; + else if (gScreenFlags & SCREEN_FLAGS_SCENARIO_EDITOR) + return "scenario_editor"; + else if (gScreenFlags & SCREEN_FLAGS_TRACK_DESIGNER) + return "track_designer"; + else if (gScreenFlags & SCREEN_FLAGS_TRACK_MANAGER) + return "track_manager"; + return "normal"; + } + void captureImage(const DukValue& options) { auto ctx = GetContext()->GetScriptEngine().GetContext(); @@ -439,6 +453,7 @@ namespace OpenRCT2::Scripting dukglue_register_property(ctx, &ScContext::configuration_get, nullptr, "configuration"); dukglue_register_property(ctx, &ScContext::sharedStorage_get, nullptr, "sharedStorage"); dukglue_register_method(ctx, &ScContext::getParkStorage, "getParkStorage"); + dukglue_register_property(ctx, &ScContext::mode_get, nullptr, "mode"); dukglue_register_method(ctx, &ScContext::captureImage, "captureImage"); dukglue_register_method(ctx, &ScContext::getObject, "getObject"); dukglue_register_method(ctx, &ScContext::getAllObjects, "getAllObjects"); From af340cbf34d44bfa4bf6f5d5bbbf0d9e956fa186 Mon Sep 17 00:00:00 2001 From: Ted John Date: Sun, 20 Feb 2022 19:27:43 +0000 Subject: [PATCH 06/13] Add API for toolbox menu items --- distribution/openrct2.d.ts | 8 +++ src/openrct2-ui/scripting/CustomMenu.cpp | 1 - src/openrct2-ui/scripting/CustomMenu.h | 11 ++- src/openrct2-ui/scripting/ScUi.hpp | 17 ++++- src/openrct2-ui/windows/TitleMenu.cpp | 91 ++++++++++++++++++++---- src/openrct2-ui/windows/TopToolbar.cpp | 24 +++++-- 6 files changed, 131 insertions(+), 21 deletions(-) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index a372e15268..fbf5bc9d95 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -2124,6 +2124,14 @@ declare global { registerMenuItem(text: string, callback: () => void): void; + /** + * Registers a new item in the toolbox menu on the title screen. + * Only available to intransient plugins. + * @param text The menu item text. + * @param callback The function to call when the menu item is clicked. + */ + registerToolboxMenuItem(text: string, callback: () => void): void; + registerShortcut(desc: ShortcutDesc): void; } diff --git a/src/openrct2-ui/scripting/CustomMenu.cpp b/src/openrct2-ui/scripting/CustomMenu.cpp index 4faf3389dd..b84633fc7b 100644 --- a/src/openrct2-ui/scripting/CustomMenu.cpp +++ b/src/openrct2-ui/scripting/CustomMenu.cpp @@ -279,7 +279,6 @@ namespace OpenRCT2::Scripting duk_error(scriptEngine.GetContext(), DUK_ERR_ERROR, "Invalid parameters."); } } - } // namespace OpenRCT2::Scripting #endif diff --git a/src/openrct2-ui/scripting/CustomMenu.h b/src/openrct2-ui/scripting/CustomMenu.h index 83e15749da..b34d33638f 100644 --- a/src/openrct2-ui/scripting/CustomMenu.h +++ b/src/openrct2-ui/scripting/CustomMenu.h @@ -23,15 +23,24 @@ enum class CursorID : uint8_t; namespace OpenRCT2::Scripting { + enum class CustomToolbarMenuItemKind + { + Standard, + Toolbox, + }; + class CustomToolbarMenuItem { public: std::shared_ptr Owner; + CustomToolbarMenuItemKind Kind; std::string Text; DukValue Callback; - CustomToolbarMenuItem(std::shared_ptr owner, const std::string& text, DukValue callback) + CustomToolbarMenuItem( + std::shared_ptr owner, CustomToolbarMenuItemKind kind, const std::string& text, DukValue callback) : Owner(owner) + , Kind(kind) , Text(text) , Callback(callback) { diff --git a/src/openrct2-ui/scripting/ScUi.hpp b/src/openrct2-ui/scripting/ScUi.hpp index 5f8a55c85a..17050d24cc 100644 --- a/src/openrct2-ui/scripting/ScUi.hpp +++ b/src/openrct2-ui/scripting/ScUi.hpp @@ -313,7 +313,21 @@ namespace OpenRCT2::Scripting { auto& execInfo = _scriptEngine.GetExecInfo(); auto owner = execInfo.GetCurrentPlugin(); - CustomMenuItems.emplace_back(owner, text, callback); + CustomMenuItems.emplace_back(owner, CustomToolbarMenuItemKind::Standard, text, callback); + } + + void registerToolboxMenuItem(const std::string& text, DukValue callback) + { + auto& execInfo = _scriptEngine.GetExecInfo(); + auto owner = execInfo.GetCurrentPlugin(); + if (owner->GetMetadata().Type == PluginType::Intransient) + { + CustomMenuItems.emplace_back(owner, CustomToolbarMenuItemKind::Toolbox, text, callback); + } + else + { + duk_error(_scriptEngine.GetContext(), DUK_ERR_ERROR, "Plugin must be intransient."); + } } void registerShortcut(DukValue desc) @@ -364,6 +378,7 @@ namespace OpenRCT2::Scripting dukglue_register_method(ctx, &ScUi::showScenarioSelect, "showScenarioSelect"); dukglue_register_method(ctx, &ScUi::activateTool, "activateTool"); dukglue_register_method(ctx, &ScUi::registerMenuItem, "registerMenuItem"); + dukglue_register_method(ctx, &ScUi::registerToolboxMenuItem, "registerToolboxMenuItem"); dukglue_register_method(ctx, &ScUi::registerShortcut, "registerShortcut"); } diff --git a/src/openrct2-ui/windows/TitleMenu.cpp b/src/openrct2-ui/windows/TitleMenu.cpp index 8698db5f15..1cc73987f0 100644 --- a/src/openrct2-ui/windows/TitleMenu.cpp +++ b/src/openrct2-ui/windows/TitleMenu.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -31,6 +32,16 @@ enum { WIDX_NEW_VERSION, }; +enum +{ + DDIDX_SCENARIO_EDITOR, + DDIDX_CONVERT_SAVED_GAME, + DDIDX_TRACK_DESIGNER, + DDIDX_TRACK_MANAGER, + DDIDX_OPEN_CONTENT_FOLDER, + DDIDX_CUSTOM_BEGIN = 6, +}; + static ScreenRect _filterRect; static constexpr ScreenSize MenuButtonDims = { 82, 82 }; static constexpr ScreenSize UpdateButtonDims = { MenuButtonDims.width * 4, 28 }; @@ -167,36 +178,89 @@ static void WindowTitleMenuMousedown(rct_window* w, rct_widgetindex widgetIndex, { if (widgetIndex == WIDX_GAME_TOOLS) { - gDropdownItems[0].Format = STR_SCENARIO_EDITOR; - gDropdownItems[1].Format = STR_CONVERT_SAVED_GAME_TO_SCENARIO; - gDropdownItems[2].Format = STR_ROLLER_COASTER_DESIGNER; - gDropdownItems[3].Format = STR_TRACK_DESIGNS_MANAGER; - gDropdownItems[4].Format = STR_OPEN_USER_CONTENT_FOLDER; + int32_t i = 0; + gDropdownItems[i++].Format = STR_SCENARIO_EDITOR; + gDropdownItems[i++].Format = STR_CONVERT_SAVED_GAME_TO_SCENARIO; + gDropdownItems[i++].Format = STR_ROLLER_COASTER_DESIGNER; + gDropdownItems[i++].Format = STR_TRACK_DESIGNS_MANAGER; + gDropdownItems[i++].Format = STR_OPEN_USER_CONTENT_FOLDER; + +#ifdef ENABLE_SCRIPTING + auto hasCustomItems = false; + const auto& customMenuItems = OpenRCT2::Scripting::CustomMenuItems; + if (!customMenuItems.empty()) + { + for (const auto& item : customMenuItems) + { + if (item.Kind == OpenRCT2::Scripting::CustomToolbarMenuItemKind::Toolbox) + { + // Add seperator + if (!hasCustomItems) + { + hasCustomItems = true; + gDropdownItems[i++].Format = STR_EMPTY; + } + + gDropdownItems[i].Format = STR_STRING; + auto sz = item.Text.c_str(); + std::memcpy(&gDropdownItems[i].Args, &sz, sizeof(const char*)); + i++; + } + } + } +#endif + + int32_t yOffset = 0; + if (i > 5) + { + yOffset = -(widget->height() + 5 + (i * 12)); + } + WindowDropdownShowText( - { w->windowPos.x + widget->left, w->windowPos.y + widget->top }, widget->height() + 1, TRANSLUCENT(w->colours[0]), - Dropdown::Flag::StayOpen, 5); + { w->windowPos.x + widget->left, w->windowPos.y + widget->top + yOffset }, widget->height() + 1, + TRANSLUCENT(w->colours[0]), Dropdown::Flag::StayOpen, i); } } +static void InvokeCustomToolboxMenuItem(size_t index) +{ +#ifdef ENABLE_SCRIPTING + const auto& customMenuItems = OpenRCT2::Scripting::CustomMenuItems; + size_t i = 0; + for (const auto& item : customMenuItems) + { + if (item.Kind == OpenRCT2::Scripting::CustomToolbarMenuItemKind::Toolbox) + { + if (i == index) + { + item.Invoke(); + break; + } + i++; + } + } +#endif +} + static void WindowTitleMenuDropdown(rct_window* w, rct_widgetindex widgetIndex, int32_t dropdownIndex) { if (widgetIndex == WIDX_GAME_TOOLS) { switch (dropdownIndex) { - case 0: + case DDIDX_SCENARIO_EDITOR: Editor::Load(); break; - case 1: + case DDIDX_CONVERT_SAVED_GAME: Editor::ConvertSaveToScenario(); break; - case 2: + case DDIDX_TRACK_DESIGNER: Editor::LoadTrackDesigner(); break; - case 3: + case DDIDX_TRACK_MANAGER: Editor::LoadTrackManager(); break; - case 4: + case DDIDX_OPEN_CONTENT_FOLDER: { auto context = OpenRCT2::GetContext(); auto env = context->GetPlatformEnvironment(); @@ -204,6 +268,9 @@ static void WindowTitleMenuDropdown(rct_window* w, rct_widgetindex widgetIndex, uiContext->OpenFolder(env->GetDirectoryPath(OpenRCT2::DIRBASE::USER)); break; } + default: + InvokeCustomToolboxMenuItem(dropdownIndex - DDIDX_CUSTOM_BEGIN); + break; } } } diff --git a/src/openrct2-ui/windows/TopToolbar.cpp b/src/openrct2-ui/windows/TopToolbar.cpp index 02bdfe8490..117071df2f 100644 --- a/src/openrct2-ui/windows/TopToolbar.cpp +++ b/src/openrct2-ui/windows/TopToolbar.cpp @@ -3315,10 +3315,13 @@ static void TopToolbarInitMapMenu(rct_window* w, rct_widget* widget) gDropdownItems[i++].Format = STR_EMPTY; for (const auto& item : customMenuItems) { - gDropdownItems[i].Format = STR_STRING; - auto sz = item.Text.c_str(); - std::memcpy(&gDropdownItems[i].Args, &sz, sizeof(const char*)); - i++; + if (item.Kind == OpenRCT2::Scripting::CustomToolbarMenuItemKind::Standard) + { + gDropdownItems[i].Format = STR_STRING; + auto sz = item.Text.c_str(); + std::memcpy(&gDropdownItems[i].Args, &sz, sizeof(const char*)); + i++; + } } } #endif @@ -3356,9 +3359,18 @@ static void TopToolbarMapMenuDropdown(int16_t dropdownIndex) #ifdef ENABLE_SCRIPTING const auto& customMenuItems = OpenRCT2::Scripting::CustomMenuItems; auto customIndex = static_cast(dropdownIndex - customStartIndex); - if (customMenuItems.size() > customIndex) + size_t i = 0; + for (const auto& item : customMenuItems) { - customMenuItems[customIndex].Invoke(); + if (item.Kind == OpenRCT2::Scripting::CustomToolbarMenuItemKind::Standard) + { + if (i == customIndex) + { + item.Invoke(); + break; + } + i++; + } } #endif } From 8f34029e89a9b17bd0c92aeb5685a8dcf4a25aa7 Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 21 Feb 2022 00:40:00 +0000 Subject: [PATCH 07/13] Re-create windows if loading park on title screen --- src/openrct2-ui/scripting/ScTitleSequence.hpp | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/openrct2-ui/scripting/ScTitleSequence.hpp b/src/openrct2-ui/scripting/ScTitleSequence.hpp index f04d942326..60e0c4b4be 100644 --- a/src/openrct2-ui/scripting/ScTitleSequence.hpp +++ b/src/openrct2-ui/scripting/ScTitleSequence.hpp @@ -214,7 +214,14 @@ namespace OpenRCT2::Scripting parkImporter->Import(); auto old = gLoadKeepWindowsOpen; - gLoadKeepWindowsOpen = true; + + // Unless we are already in the game, we have to re-create the windows + // so that the game toolbars are created. + if (gScreenFlags == SCREEN_FLAGS_PLAYING) + { + gLoadKeepWindowsOpen = true; + } + if (isScenario) scenario_begin(); else From 3864795a087b11d079a73ade10f1ae17997dc570 Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 21 Feb 2022 00:40:31 +0000 Subject: [PATCH 08/13] Improve safety on event handlers from windows --- src/openrct2/scripting/ScriptEngine.cpp | 3 ++- src/openrct2/scripting/ScriptEngine.h | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 7cf2fa98b4..8d867cd180 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -868,8 +868,9 @@ DukValue ScriptEngine::ExecutePluginCall( return ExecutePluginCall(plugin, func, dukUndefined, args, isGameStateMutable); } +// Must pass plugin by-value, a JS function could destroy the original reference DukValue ScriptEngine::ExecutePluginCall( - const std::shared_ptr& plugin, const DukValue& func, const DukValue& thisValue, const std::vector& args, + std::shared_ptr plugin, const DukValue& func, const DukValue& thisValue, const std::vector& args, bool isGameStateMutable) { DukStackFrame frame(_context); diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 01633ca8f7..add4c00f99 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -220,8 +220,8 @@ namespace OpenRCT2::Scripting const std::shared_ptr& plugin, const DukValue& func, const std::vector& args, bool isGameStateMutable); DukValue ExecutePluginCall( - const std::shared_ptr& plugin, const DukValue& func, const DukValue& thisValue, - const std::vector& args, bool isGameStateMutable); + std::shared_ptr plugin, const DukValue& func, const DukValue& thisValue, const std::vector& args, + bool isGameStateMutable); void LogPluginInfo(const std::shared_ptr& plugin, std::string_view message); From cfc1b622c2bf7c3d5ccaf3f102f0e65b6fea9a93 Mon Sep 17 00:00:00 2001 From: Ted John Date: Tue, 22 Feb 2022 21:38:41 +0000 Subject: [PATCH 09/13] Update changelog --- distribution/changelog.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/distribution/changelog.txt b/distribution/changelog.txt index 15431e96f9..d351dec31d 100644 --- a/distribution/changelog.txt +++ b/distribution/changelog.txt @@ -16,6 +16,8 @@ - Feature: [#16097] The Looping Roller Coaster can now draw all elements from the LIM Launched Roller Coaster. - Feature: [#16132, #16389] The Corkscrew, Twister and Vertical Drop Roller Coasters can now draw inline twists. - Feature: [#16144] [Plugin] Add ImageManager to API. +- Feature: [#16707] [Plugin] Implement intransient plugins. +- Feature: [#16707] [Plugin] New API for current mode, map.change hook and toolbox menu items on title screen. - Feature: [#16731] [Plugin] New API for fetching and manipulating a staff member's patrol area. - Feature: [#16800] [Plugin] Add lift hill speed properties to API. - Feature: [#16806] Parkobj can load sprites from RCT image archives. From b579d151191d85e2f3b641f9fe72124ec1b72ef9 Mon Sep 17 00:00:00 2001 From: Ted John Date: Tue, 22 Feb 2022 21:54:00 +0000 Subject: [PATCH 10/13] Update scripting readme --- distribution/scripting.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/distribution/scripting.md b/distribution/scripting.md index e780647960..984748cfe9 100644 --- a/distribution/scripting.md +++ b/distribution/scripting.md @@ -10,14 +10,17 @@ Each script is a single physical javascript file within the `plugin` directory i OpenRCT2 will load every single file with the extension `.js` in this directory recursively. So if you want to prevent a plug-in from being used, you must move it outside this directory, or rename it so the filename does not end with `.js`. -There are two types of scripts: +There are three types of scripts: * Local * Remote +* Intransient Local scripts can **not** alter the game state. This allows each player to enable any local script for their own game without other players needing to also enable the same script. These scripts tend to provide extra tools for productivity, or new windows containing information. Remote scripts on the other hand can alter the game state in certain contexts, thus must be enabled for every player in a multiplayer game. Players **cannot** enable or disable remote scripts for multiplayer servers they join. Instead the server will upload any remote scripts that have been enabled on the server to each player. This allows servers to enable scripts without players needing to manually download or enable the same script on their end. +Intransient scripts are similar to local scripts, in that they can **not** alter the game state. However they are loaded at the very start of launching OpenRCT2 and remain loaded until shutdown. This allows the plugin to provide functionality across different screens such as the title screen. + The authors must also define a licence for the plug-in, making it clear to the community whether that plug-in can be altered, copied, etc. A good reference material is listed on [ChooseALlicense](https://choosealicense.com/appendix/), try to pick one of them and use its corresponding identifier, as listed on [SPDX](https://spdx.org/licenses/). ## Writing Scripts @@ -92,7 +95,7 @@ Debugging has not yet been implemented, but is planned. In the meantime, you can > What does the error 'Game state is not mutable in this context' mean? -This means you are attempting to modify the game state (e.g. change the park, map or guests etc.) in a context where you should not be doing so. This might be because your script is defined as `local`, meaning it must work independently of other players, not having the script enabled, or a remote script attempting to modify the game in the main function or a user interface event. +This means you are attempting to modify the game state (e.g. change the park, map or guests etc.) in a context where you should not be doing so. This might be because your script is defined as `local` or `intransient`, meaning it must work independently of other players, not having the script enabled, or a remote script attempting to modify the game in the main function or a user interface event. Any changes to the game state must be synchronised across all players so that the same changes happen on the same tick for every player. This prevents the game going out of sync. To do this you must only change game state in a compatible hook such as `interval.day` or in the execute method of a game action. Game actions allow players to make specific changes to the game providing they have the correct permissions and the server allows it. From e4a10b8eb42005034bfea2c9fb6334f74a918fcb Mon Sep 17 00:00:00 2001 From: Ted John Date: Sun, 27 Feb 2022 15:48:43 +0000 Subject: [PATCH 11/13] Fix and improve plugin startup / shutdown --- src/openrct2/Context.cpp | 8 +++++ src/openrct2/scripting/ScriptEngine.cpp | 44 ++++++++++++++----------- src/openrct2/scripting/ScriptEngine.h | 8 ++--- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/openrct2/Context.cpp b/src/openrct2/Context.cpp index 49be7bf598..ea50eff39e 100644 --- a/src/openrct2/Context.cpp +++ b/src/openrct2/Context.cpp @@ -174,6 +174,10 @@ namespace OpenRCT2 // NOTE: We must shutdown all systems here before Instance is set back to null. // If objects use GetContext() in their destructor things won't go well. +#ifdef ENABLE_SCRIPTING + _scriptEngine.StopUnloadRegisterAllPlugins(); +#endif + GameActions::ClearQueue(); #ifndef DISABLE_NETWORK _network.Close(); @@ -503,6 +507,10 @@ namespace OpenRCT2 _gameState = std::make_unique(); _gameState->InitAll(DEFAULT_MAP_SIZE); +#ifdef ENABLE_SCRIPTING + _scriptEngine.Initialise(); +#endif + _titleScreen = std::make_unique(*_gameState); _uiContext->Initialise(); diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index 8d867cd180..af76646e45 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -386,6 +386,9 @@ ScriptEngine::ScriptEngine(InteractiveConsole& console, IPlatformEnvironment& en void ScriptEngine::Initialise() { + if (_initialised) + throw std::runtime_error("Script engine already initialised."); + auto ctx = static_cast(_context); ScCheats::Register(ctx); ScClimate::Register(ctx); @@ -445,11 +448,6 @@ void ScriptEngine::Initialise() void ScriptEngine::RefreshPlugins() { - if (!_initialised) - { - Initialise(); - } - // Get a list of removed and added plugin files auto pluginFiles = GetPluginFiles(); std::vector plugins; @@ -481,9 +479,6 @@ void ScriptEngine::RefreshPlugins() { SetupHotReloading(); } - - // Start any new intransient plugins - StartIntransientPlugins(); } std::vector ScriptEngine::GetPluginFiles() const @@ -568,6 +563,8 @@ void ScriptEngine::StartIntransientPlugins() StartPlugin(plugin); } } + + _intransientPluginsStarted = true; } void ScriptEngine::StopUnloadRegisterAllPlugins() @@ -811,23 +808,32 @@ void ScriptEngine::Tick() { PROFILED_FUNCTION(); - if (!_initialised) - { - Initialise(); - RefreshPlugins(); - } - - if (_transientPluginsEnabled && !_transientPluginsStarted) - { - StartTransientPlugins(); - } - + 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) diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index add4c00f99..2b4208ed72 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -148,6 +148,7 @@ namespace OpenRCT2::Scripting bool _hotReloadingInitialised{}; bool _transientPluginsEnabled{}; bool _transientPluginsStarted{}; + bool _intransientPluginsStarted{}; std::queue, std::string>> _evalQueue; std::vector> _plugins; uint32_t _lastHotReloadCheckTick{}; @@ -210,10 +211,10 @@ namespace OpenRCT2::Scripting std::string GetParkStorageAsJSON(); void SetParkStorageFromJSON(std::string_view value); - void LoadPlugins(); - void UnloadPlugins(); + void Initialise(); void LoadTransientPlugins(); void UnloadTransientPlugins(); + void StopUnloadRegisterAllPlugins(); void Tick(); std::future Eval(const std::string& s); DukValue ExecutePluginCall( @@ -249,11 +250,11 @@ namespace OpenRCT2::Scripting # endif private: - void Initialise(); void RefreshPlugins(); std::vector GetPluginFiles() const; void UnregisterPlugin(std::string_view path); void RegisterPlugin(std::string_view path); + void CheckAndStartPlugins(); void StartIntransientPlugins(); void StartTransientPlugins(); void LoadPlugin(const std::string& path); @@ -262,7 +263,6 @@ namespace OpenRCT2::Scripting void StartPlugin(std::shared_ptr plugin); void StopPlugin(std::shared_ptr plugin); void ReloadPlugin(std::shared_ptr plugin); - void StopUnloadRegisterAllPlugins(); static bool ShouldLoadScript(std::string_view path); bool ShouldStartPlugin(const std::shared_ptr& plugin); void SetupHotReloading(); From 9f96b0df330aa4507019680368bb0f7f7ac3beff Mon Sep 17 00:00:00 2001 From: Ted John Date: Mon, 21 Mar 2022 22:56:21 +0000 Subject: [PATCH 12/13] Separate map.change and map.changed events Also fix network plugin logic. --- distribution/openrct2.d.ts | 3 ++- src/openrct2-ui/title/TitleSequencePlayer.cpp | 2 ++ .../windows/EditorObjectSelection.cpp | 1 + src/openrct2-ui/windows/ServerStart.cpp | 7 ++++-- src/openrct2-ui/windows/TitleMenu.cpp | 1 + src/openrct2-ui/windows/TopToolbar.cpp | 1 + src/openrct2/Game.cpp | 25 ++++++++++++++++++- src/openrct2/Game.h | 1 + src/openrct2/network/NetworkBase.cpp | 8 ++++++ src/openrct2/scripting/HookEngine.cpp | 3 ++- src/openrct2/scripting/HookEngine.h | 1 + src/openrct2/scripting/ScriptEngine.cpp | 23 +++++++++++++++-- src/openrct2/scripting/ScriptEngine.h | 1 + 13 files changed, 70 insertions(+), 7 deletions(-) diff --git a/distribution/openrct2.d.ts b/distribution/openrct2.d.ts index fbf5bc9d95..583e90b330 100644 --- a/distribution/openrct2.d.ts +++ b/distribution/openrct2.d.ts @@ -292,6 +292,7 @@ declare global { subscribe(hook: "guest.generation", callback: (e: GuestGenerationArgs) => void): IDisposable; subscribe(hook: "vehicle.crash", callback: (e: VehicleCrashArgs) => void): IDisposable; subscribe(hook: "map.save", callback: () => void): IDisposable; + subscribe(hook: "map.change", callback: () => void): IDisposable; /** * Can only be used in intransient plugins. @@ -405,7 +406,7 @@ declare global { "interval.tick" | "interval.day" | "network.chat" | "network.action" | "network.join" | "network.leave" | "ride.ratings.calculate" | "action.location" | "vehicle.crash" | - "map.changed" | "map.save"; + "map.change" | "map.changed" | "map.save"; type ExpenditureType = "ride_construction" | diff --git a/src/openrct2-ui/title/TitleSequencePlayer.cpp b/src/openrct2-ui/title/TitleSequencePlayer.cpp index 8050409ade..fe4c314004 100644 --- a/src/openrct2-ui/title/TitleSequencePlayer.cpp +++ b/src/openrct2-ui/title/TitleSequencePlayer.cpp @@ -286,6 +286,7 @@ private: auto parkHandle = TitleSequenceGetParkHandle(*_sequence, saveIndex); if (parkHandle != nullptr) { + game_notify_map_change(); loadSuccess = LoadParkFromStream(parkHandle->Stream.get(), parkHandle->HintPath); } if (loadSuccess) @@ -309,6 +310,7 @@ private: auto scenario = GetScenarioRepository()->GetByInternalName(command.Scenario); if (scenario != nullptr) { + game_notify_map_change(); loadSuccess = LoadParkFromFile(scenario->path); } if (loadSuccess) diff --git a/src/openrct2-ui/windows/EditorObjectSelection.cpp b/src/openrct2-ui/windows/EditorObjectSelection.cpp index f78617e643..47dc405c7e 100644 --- a/src/openrct2-ui/windows/EditorObjectSelection.cpp +++ b/src/openrct2-ui/windows/EditorObjectSelection.cpp @@ -337,6 +337,7 @@ public: } if (gScreenFlags & SCREEN_FLAGS_TRACK_MANAGER) { + game_notify_map_change(); game_unload_scripts(); title_load(); } diff --git a/src/openrct2-ui/windows/ServerStart.cpp b/src/openrct2-ui/windows/ServerStart.cpp index c1765171e5..b64be43961 100644 --- a/src/openrct2-ui/windows/ServerStart.cpp +++ b/src/openrct2-ui/windows/ServerStart.cpp @@ -126,7 +126,7 @@ static void WindowServerStartClose(rct_window* w) static void WindowServerStartScenarioselectCallback(const utf8* path) { - network_set_password(_password); + game_notify_map_change(); if (context_load_park_from_file(path)) { network_begin_server(gConfigNetwork.default_port, gConfigNetwork.listen_address.c_str()); @@ -135,8 +135,10 @@ static void WindowServerStartScenarioselectCallback(const utf8* path) static void WindowServerStartLoadsaveCallback(int32_t result, const utf8* path) { - if (result == MODAL_RESULT_OK && context_load_park_from_file(path)) + if (result == MODAL_RESULT_OK) { + game_notify_map_change(); + context_load_park_from_file(path); network_begin_server(gConfigNetwork.default_port, gConfigNetwork.listen_address.c_str()); } } @@ -185,6 +187,7 @@ static void WindowServerStartMouseup(rct_window* w, rct_widgetindex widgetIndex) w->Invalidate(); break; case WIDX_START_SERVER: + network_set_password(_password); WindowScenarioselectOpen(WindowServerStartScenarioselectCallback, false); break; case WIDX_LOAD_SERVER: diff --git a/src/openrct2-ui/windows/TitleMenu.cpp b/src/openrct2-ui/windows/TitleMenu.cpp index 1cc73987f0..285435ffbc 100644 --- a/src/openrct2-ui/windows/TitleMenu.cpp +++ b/src/openrct2-ui/windows/TitleMenu.cpp @@ -117,6 +117,7 @@ rct_window* WindowTitleMenuOpen() static void WindowTitleMenuScenarioselectCallback(const utf8* path) { + game_notify_map_change(); OpenRCT2::GetContext()->LoadParkFromFile(path, false, true); game_load_scripts(); game_notify_map_changed(); diff --git a/src/openrct2-ui/windows/TopToolbar.cpp b/src/openrct2-ui/windows/TopToolbar.cpp index 117071df2f..cd0feb9a25 100644 --- a/src/openrct2-ui/windows/TopToolbar.cpp +++ b/src/openrct2-ui/windows/TopToolbar.cpp @@ -535,6 +535,7 @@ static void WindowTopToolbarMousedown(rct_window* w, rct_widgetindex widgetIndex static void WindowTopToolbarScenarioselectCallback(const utf8* path) { window_close_by_class(WC_EDITOR_OBJECT_SELECTION); + game_notify_map_change(); GetContext()->LoadParkFromFile(path, false, true); game_load_scripts(); game_notify_map_changed(); diff --git a/src/openrct2/Game.cpp b/src/openrct2/Game.cpp index 85bfb40460..03492494ca 100644 --- a/src/openrct2/Game.cpp +++ b/src/openrct2/Game.cpp @@ -89,6 +89,10 @@ uint32_t gCurrentRealTimeTicks; rct_string_id gGameCommandErrorTitle; rct_string_id gGameCommandErrorText; +#ifdef ENABLE_SCRIPTING +static bool _mapChangedExpected; +#endif + using namespace OpenRCT2; void game_reset_speed() @@ -513,6 +517,22 @@ void game_unload_scripts() #endif } +void game_notify_map_change() +{ +#ifdef ENABLE_SCRIPTING + // Ensure we don't get a two lots of change events + if (_mapChangedExpected) + return; + + using namespace OpenRCT2::Scripting; + + auto& scriptEngine = GetContext()->GetScriptEngine(); + auto& hookEngine = scriptEngine.GetHookEngine(); + hookEngine.Call(HOOK_TYPE::MAP_CHANGE, false); + _mapChangedExpected = true; +#endif +} + void game_notify_map_changed() { #ifdef ENABLE_SCRIPTING @@ -520,7 +540,8 @@ void game_notify_map_changed() auto& scriptEngine = GetContext()->GetScriptEngine(); auto& hookEngine = scriptEngine.GetHookEngine(); - hookEngine.Call(HOOK_TYPE::MAP_CHANGE, false); + hookEngine.Call(HOOK_TYPE::MAP_CHANGED, false); + _mapChangedExpected = false; #endif } @@ -703,6 +724,7 @@ static void game_load_or_quit_no_save_prompt_callback(int32_t result, const utf8 { if (result == MODAL_RESULT_OK) { + game_notify_map_change(); game_unload_scripts(); window_close_by_class(WC_EDITOR_OBJECT_SELECTION); context_load_park_from_file(path); @@ -748,6 +770,7 @@ void game_load_or_quit_no_save_prompt() } gGameSpeed = 1; gFirstTimeSaving = true; + game_notify_map_change(); game_unload_scripts(); title_load(); break; diff --git a/src/openrct2/Game.h b/src/openrct2/Game.h index 1e8465e114..6439e18ba9 100644 --- a/src/openrct2/Game.h +++ b/src/openrct2/Game.h @@ -160,6 +160,7 @@ void load_from_sv6(const char* path); void game_load_init(); void game_load_scripts(); void game_unload_scripts(); +void game_notify_map_change(); void game_notify_map_changed(); void pause_toggle(); bool game_is_paused(); diff --git a/src/openrct2/network/NetworkBase.cpp b/src/openrct2/network/NetworkBase.cpp index 026cfc27c9..3f5abeac91 100644 --- a/src/openrct2/network/NetworkBase.cpp +++ b/src/openrct2/network/NetworkBase.cpp @@ -203,6 +203,11 @@ void NetworkBase::Close() _pendingPlayerLists.clear(); _pendingPlayerInfo.clear(); +# ifdef ENABLE_SCRIPTING + auto& scriptEngine = GetContext().GetScriptEngine(); + scriptEngine.RemoveNetworkPlugins(); +# endif + gfx_invalidate_screen(); _requireClose = false; @@ -2681,6 +2686,9 @@ void NetworkBase::Client_Handle_MAP([[maybe_unused]] NetworkConnection& connecti GameActions::ResumeQueue(); context_force_close_window_by_class(WC_NETWORK_STATUS); + game_unload_scripts(); + game_notify_map_change(); + bool has_to_free = false; uint8_t* data = &chunk_buffer[0]; size_t data_size = size; diff --git a/src/openrct2/scripting/HookEngine.cpp b/src/openrct2/scripting/HookEngine.cpp index a1f9d2302d..19412f2148 100644 --- a/src/openrct2/scripting/HookEngine.cpp +++ b/src/openrct2/scripting/HookEngine.cpp @@ -32,6 +32,7 @@ static const EnumMap HooksLookupTable({ { "guest.generation", HOOK_TYPE::GUEST_GENERATION }, { "vehicle.crash", HOOK_TYPE::VEHICLE_CRASH }, { "map.change", HOOK_TYPE::MAP_CHANGE }, + { "map.changed", HOOK_TYPE::MAP_CHANGED }, { "map.save", HOOK_TYPE::MAP_SAVE }, }); @@ -100,7 +101,7 @@ bool HookEngine::HasSubscriptions(HOOK_TYPE type) const bool HookEngine::IsValidHookForPlugin(HOOK_TYPE type, Plugin& plugin) const { - if (type == HOOK_TYPE::MAP_CHANGE && plugin.GetMetadata().Type != PluginType::Intransient) + if (type == HOOK_TYPE::MAP_CHANGED && plugin.GetMetadata().Type != PluginType::Intransient) { return false; } diff --git a/src/openrct2/scripting/HookEngine.h b/src/openrct2/scripting/HookEngine.h index 832388ace2..c2667b5fe3 100644 --- a/src/openrct2/scripting/HookEngine.h +++ b/src/openrct2/scripting/HookEngine.h @@ -41,6 +41,7 @@ namespace OpenRCT2::Scripting GUEST_GENERATION, VEHICLE_CRASH, MAP_CHANGE, + MAP_CHANGED, MAP_SAVE, COUNT, UNDEFINED = -1, diff --git a/src/openrct2/scripting/ScriptEngine.cpp b/src/openrct2/scripting/ScriptEngine.cpp index af76646e45..7d91c10924 100644 --- a/src/openrct2/scripting/ScriptEngine.cpp +++ b/src/openrct2/scripting/ScriptEngine.cpp @@ -455,7 +455,10 @@ void ScriptEngine::RefreshPlugins() std::vector addedPlugins; for (const auto& plugin : _plugins) { - plugins.push_back(std::string(plugin->GetPath())); + if (plugin->HasPath()) + { + plugins.push_back(std::string(plugin->GetPath())); + } } std::set_difference( plugins.begin(), plugins.end(), pluginFiles.begin(), pluginFiles.end(), std::back_inserter(removedPlugins)); @@ -919,7 +922,23 @@ void ScriptEngine::AddNetworkPlugin(std::string_view code) { auto plugin = std::make_shared(_context, std::string()); plugin->SetCode(code); - LoadPlugin(plugin); + _plugins.push_back(plugin); +} + +void ScriptEngine::RemoveNetworkPlugins() +{ + auto it = _plugins.begin(); + while (it != _plugins.end()) + { + if (!(*it)->HasPath()) + { + it = _plugins.erase(it); + } + else + { + it++; + } + } } GameActions::Result ScriptEngine::QueryOrExecuteCustomGameAction(std::string_view id, std::string_view args, bool isExecute) diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 2b4208ed72..67709c8a64 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -232,6 +232,7 @@ namespace OpenRCT2::Scripting } void AddNetworkPlugin(std::string_view code); + void RemoveNetworkPlugins(); [[nodiscard]] GameActions::Result QueryOrExecuteCustomGameAction( std::string_view id, std::string_view args, bool isExecute); From a9d2c87d65362a98c7b0228d235209688813a619 Mon Sep 17 00:00:00 2001 From: Ted John Date: Tue, 22 Mar 2022 21:11:51 +0000 Subject: [PATCH 13/13] Bump plugin API version --- src/openrct2/scripting/ScriptEngine.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/openrct2/scripting/ScriptEngine.h b/src/openrct2/scripting/ScriptEngine.h index 67709c8a64..a2a16e4c3f 100644 --- a/src/openrct2/scripting/ScriptEngine.h +++ b/src/openrct2/scripting/ScriptEngine.h @@ -46,7 +46,7 @@ namespace OpenRCT2 namespace OpenRCT2::Scripting { - static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 50; + static constexpr int32_t OPENRCT2_PLUGIN_API_VERSION = 51; // Versions marking breaking changes. static constexpr int32_t API_VERSION_33_PEEP_DEPRECATION = 33;