Repository: OpenRCT2/OpenRCT2 (2026-04-03T10:44:56Z)

Issue:
new script engine breaks when one event has multiple event listeners
### Operating System

Ubuntu ( via [openrct2/openrct2-cli:develop](https://hub.docker.com/r/openrct2/openrct2-cli))

### OpenRCT2 build

v0.4.32 (02c18b6 on develop)

### Base game

RollerCoaster Tycoon 2

### Area(s) with this issue?

This is a development issue

### Describe the issue

If the same event is subscribed to two or more times, either by a single plugin or across multiple plugins, the game crashes when the event is triggered. This limitation did not exist in the previous duktape-powered scripting engine.

### Steps to reproduce

1. Install the following plugin 
```
registerPlugin({
    name: 'example',
    version: '0.2.4',
    authors: ['Cory Sanin'],
    type: 'remote',
    licence: 'GPL-3.0',
    minApiVersion: 17,
    targetApiVersion: 77,
    main: function () {
        context.subscribe('network.join', function (e) {
            console.log('1');
        });

        context.subscribe('network.join', function (e) {
            console.log('2');
        });
    }
});
```
2. Start a multiplayer server
3. Join the server, triggering `network.join`
4. server exits 139


### Attachments

_No response_


Discussion:
Should get tagged https://github.com/OpenRCT2/OpenRCT2/labels/plug-in
@CorySanin can you test #26315 ?
@janisozaur https://github.com/OpenRCT2/OpenRCT2/pull/26315 fixes it!



Patch:
diff --git a/src/openrct2/scripting/HookEngine.cpp b/src/openrct2/scripting/HookEngine.cpp
index 12632981e6a7..daf5b4697759 100644
--- a/src/openrct2/scripting/HookEngine.cpp
+++ b/src/openrct2/scripting/HookEngine.cpp
@@ -122,7 +122,16 @@ void HookEngine::Call(HookType type, const JSValue arg, bool isGameStateMutable,
     auto& hookList = GetHookList(type);
     for (auto& hook : hookList.Hooks)
     {
-        _scriptEngine.ExecutePluginCall(hook.Owner, hook.Function.callback, { arg }, isGameStateMutable, keepArgsAlive);
+        JSContext* ctx = hook.Owner ? hook.Owner->GetContext() : _scriptEngine.GetContext();
+        _scriptEngine.ExecutePluginCall(
+            hook.Owner, hook.Function.callback, { JS_DupValue(ctx, arg) }, isGameStateMutable, false);
+    }
+
+    if (!keepArgsAlive)
+    {
+        // One final free as we are the "owner" of the passed arg
+        JSContext* ctx = _scriptEngine.GetContext();
+        JS_FreeValue(ctx, arg);
     }
 }
 


Test patch:
diff --git a/test/tests/CMakeLists.txt b/test/tests/CMakeLists.txt
index 483a54d1a50d..42f8c7bf12e6 100644
--- a/test/tests/CMakeLists.txt
+++ b/test/tests/CMakeLists.txt
@@ -30,6 +30,7 @@ set(test_files
    "${CMAKE_CURRENT_SOURCE_DIR}/S6ImportExportTests.cpp"
    "${CMAKE_CURRENT_SOURCE_DIR}/SawyerCodingTest.cpp"
    "${CMAKE_CURRENT_SOURCE_DIR}/ScenarioPatcherTests.cpp"
+   "${CMAKE_CURRENT_SOURCE_DIR}/ScriptingTests.cpp"
    "${CMAKE_CURRENT_SOURCE_DIR}/StringTest.cpp"
    "${CMAKE_CURRENT_SOURCE_DIR}/TestData.cpp"
    "${CMAKE_CURRENT_SOURCE_DIR}/TestData.h"
diff --git a/test/tests/ScriptingTests.cpp b/test/tests/ScriptingTests.cpp
new file mode 100644
index 000000000000..712a7914fbca
--- /dev/null
+++ b/test/tests/ScriptingTests.cpp
@@ -0,0 +1,76 @@
+/*****************************************************************************
+ * Copyright (c) 2014-2026 OpenRCT2 developers
+ *
+ * For a complete list of all authors, please refer to contributors.md
+ * Interested in contributing? Visit https://github.com/OpenRCT2/OpenRCT2
+ *
+ * OpenRCT2 is licensed under the GNU General Public License version 3.
+ *****************************************************************************/
+
+#include <gtest/gtest.h>
+#include <openrct2/Context.h>
+#include <openrct2/GameState.h>
+#include <openrct2/OpenRCT2.h>
+#include <openrct2/scripting/ScriptEngine.h>
+#include <quickjs.h>
+
+using namespace OpenRCT2;
+using namespace OpenRCT2::Scripting;
+
+class ScriptingTests : public testing::Test
+{
+protected:
+    void SetUp() override
+    {
+        gOpenRCT2Headless = true;
+        gOpenRCT2NoGraphics = true;
+        _context = CreateContext();
+        _context->Initialise();
+    }
+
+    std::unique_ptr<IContext> _context;
+};
+
+#ifdef ENABLE_SCRIPTING
+
+TEST_F(ScriptingTests, MultipleSubscribersToSameEventShouldNotCrash)
+{
+    auto& scriptEngine = static_cast<ScriptEngine&>(_context->GetScriptEngine());
+
+    // Register a plugin that subscribes twice to the same event
+    const char* pluginCode = R"(
+        registerPlugin({
+            name: 'test-plugin-multiple-subscribers',
+            version: '1.0.0',
+            authors: ['openrct2-test'],
+            type: 'remote',
+            licence: 'MIT',
+            minApiVersion: 110, // deliberately the version before quickjs
+            targetApiVersion: 110,
+            main: function () {
+                context.subscribe('interval.tick', function (e) {
+                    // first subscriber
+                });
+                context.subscribe('interval.tick', function (e) {
+                    // second subscriber
+                });
+            }
+        });
+    )";
+
+    scriptEngine.AddNetworkPlugin(pluginCode);
+    scriptEngine.LoadTransientPlugins();
+    scriptEngine.Tick();
+
+    auto& hookEngine = scriptEngine.GetHookEngine();
+
+    // We need a JSValue to pass to Call.
+    JSContext* ctx = scriptEngine.GetContext();
+    JSValue arg = JS_NewObject(ctx);
+    JS_SetPropertyStr(ctx, arg, "test", JS_NewInt32(ctx, 1));
+
+    // This should NOT crash.
+    hookEngine.Call(HookType::intervalTick, arg, false);
+}
+
+#endif
diff --git a/test/tests/tests.vcxproj b/test/tests/tests.vcxproj
index b327d8798cc5..aec8785babf3 100644
--- a/test/tests/tests.vcxproj
+++ b/test/tests/tests.vcxproj
@@ -103,6 +103,7 @@
     <ClCompile Include="S6ImportExportTests.cpp" />
     <ClCompile Include="SawyerCodingTest.cpp" />
     <ClCompile Include="ScenarioPatcherTests.cpp" />
+    <ClCompile Include="ScriptingTests.cpp" />
     <ClCompile Include="TestData.cpp" />
     <ClCompile Include="tests.cpp" />
     <ClCompile Include="StringTest.cpp" />