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();