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