From 43abe9a6133da17d3dde6fddd2f581da38214983 Mon Sep 17 00:00:00 2001 From: Florian Rival Date: Tue, 7 Apr 2026 12:09:45 +0200 Subject: [PATCH 1/2] Fix hot-reloading of global object instances --- GDJS/Runtime/debugger-client/hot-reloader.ts | 12 ++-- GDJS/tests/tests/hot-reloader.js | 58 +++++++++++++++++++- 2 files changed, 65 insertions(+), 5 deletions(-) diff --git a/GDJS/Runtime/debugger-client/hot-reloader.ts b/GDJS/Runtime/debugger-client/hot-reloader.ts index fa5a7ab23b17..29c67adbcf07 100644 --- a/GDJS/Runtime/debugger-client/hot-reloader.ts +++ b/GDJS/Runtime/debugger-client/hot-reloader.ts @@ -641,12 +641,16 @@ namespace gdjs { const oldObjectDataList = HotReloader.resolveCustomObjectConfigurations( oldProjectData, - oldLayoutData ? oldLayoutData.objects : [] + oldLayoutData + ? [...oldProjectData.objects, ...oldLayoutData.objects] + : oldProjectData.objects ); const newObjectDataList = HotReloader.resolveCustomObjectConfigurations( newProjectData, - newLayoutData ? newLayoutData.objects : [] + newLayoutData + ? [...newProjectData.objects, ...newLayoutData.objects] + : newProjectData.objects ); sceneStack._stack.forEach((runtimeScene) => { @@ -949,11 +953,11 @@ namespace gdjs { } const oldObjectDataList = HotReloader.resolveCustomObjectConfigurations( oldProjectData, - oldLayoutData.objects + [...oldProjectData.objects, ...oldLayoutData.objects] ); const newObjectDataList = HotReloader.resolveCustomObjectConfigurations( newProjectData, - newLayoutData.objects + [...newProjectData.objects, ...newLayoutData.objects] ); // Re-instantiate any gdjs.RuntimeBehavior that was changed. diff --git a/GDJS/tests/tests/hot-reloader.js b/GDJS/tests/tests/hot-reloader.js index a0ea19bcc159..5552292b4bda 100644 --- a/GDJS/tests/tests/hot-reloader.js +++ b/GDJS/tests/tests/hot-reloader.js @@ -40,11 +40,12 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => { /** * Create and return a minimum working game. * @internal - * @param {{layouts?: LayoutData[], eventsBasedObjects?: EventsBasedObjectData[], resources?: ResourcesData, propertiesOverrides?: Partial}} data + * @param {{layouts?: LayoutData[], globalObjects?: ObjectData[], eventsBasedObjects?: EventsBasedObjectData[], resources?: ResourcesData, propertiesOverrides?: Partial}} data * @returns {ProjectData} */ const createProjectData = (data) => { const project = gdjs.createProjectData(data); + project.objects = data.globalObjects || []; project.eventsFunctionsExtensions.push({ name: 'MyExtension', eventsBasedObjects: data.eventsBasedObjects || [], @@ -307,6 +308,61 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => { expect(instances[1].getY()).to.be(234); }); + it('can move instances of a global object at hot-reload', async () => { + const oldProjectData = createProjectData({ + globalObjects: [createSpriteData({ name: 'MyGlobalObject' })], + layouts: [ + createSceneData({ + instances: [ + { persistentUuid: '1', name: 'MyGlobalObject', x: 111, y: 123 }, + { persistentUuid: '2', name: 'MyGlobalObject', x: 222, y: 234 }, + { persistentUuid: '3', name: 'MyGlobalObject', x: 400, y: 600 }, + ], + objects: [], + }), + ], + }); + const runtimeGame = new gdjs.RuntimeGame(oldProjectData); + const hotReloader = new gdjs.HotReloader(runtimeGame); + await runtimeGame.loadFirstAssetsAndStartBackgroundLoading( + 'Scene1', + () => {} + ); + runtimeGame._sceneStack.push('Scene1'); + const scene = runtimeGame.getSceneStack().getCurrentScene(); + if (!scene) throw new Error("Couldn't set a current scene for testing."); + + const newProjectData = createProjectData({ + globalObjects: [createSpriteData({ name: 'MyGlobalObject' })], + layouts: [ + createSceneData({ + instances: [ + { persistentUuid: '1', name: 'MyGlobalObject', x: 555, y: 678 }, + { persistentUuid: '2', name: 'MyGlobalObject', x: 222, y: 234 }, + { persistentUuid: '3', name: 'MyGlobalObject', x: 400, y: 600 }, + ], + objects: [], + }), + ], + }); + + await hotReloader._hotReloadRuntimeGame( + oldProjectData, + newProjectData, + [], + runtimeGame + ); + + const instances = scene.getInstancesOf('MyGlobalObject'); + expect(instances.length).to.be(3); + expect(instances[0].getX()).to.be(555); + expect(instances[0].getY()).to.be(678); + expect(instances[1].getX()).to.be(222); + expect(instances[1].getY()).to.be(234); + expect(instances[2].getX()).to.be(400); + expect(instances[2].getY()).to.be(600); + }); + it('can change the image of a sprite object of a scene at hot-reload', async () => { const oldProjectData = createProjectData({ layouts: [ From 84d4fa887904ba76aebe06ab4f80bd231fcb37b6 Mon Sep 17 00:00:00 2001 From: Florian Rival Date: Tue, 7 Apr 2026 12:49:18 +0200 Subject: [PATCH 2/2] Add extra test --- GDJS/tests/tests/hot-reloader.js | 52 ++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/GDJS/tests/tests/hot-reloader.js b/GDJS/tests/tests/hot-reloader.js index 5552292b4bda..a2402f76feee 100644 --- a/GDJS/tests/tests/hot-reloader.js +++ b/GDJS/tests/tests/hot-reloader.js @@ -363,6 +363,58 @@ describe('gdjs.HotReloader._hotReloadRuntimeGame', () => { expect(instances[2].getY()).to.be(600); }); + it('can move instances of both scene and global objects at hot-reload', async () => { + const oldProjectData = createProjectData({ + globalObjects: [createSpriteData({ name: 'MyGlobalObject' })], + layouts: [ + createSceneData({ + instances: [ + { persistentUuid: '1', name: 'MyObject', x: 100, y: 200 }, + { persistentUuid: '2', name: 'MyGlobalObject', x: 300, y: 400 }, + ], + }), + ], + }); + const runtimeGame = new gdjs.RuntimeGame(oldProjectData); + const hotReloader = new gdjs.HotReloader(runtimeGame); + await runtimeGame.loadFirstAssetsAndStartBackgroundLoading( + 'Scene1', + () => {} + ); + runtimeGame._sceneStack.push('Scene1'); + const scene = runtimeGame.getSceneStack().getCurrentScene(); + if (!scene) throw new Error("Couldn't set a current scene for testing."); + + const newProjectData = createProjectData({ + globalObjects: [createSpriteData({ name: 'MyGlobalObject' })], + layouts: [ + createSceneData({ + instances: [ + { persistentUuid: '1', name: 'MyObject', x: 150, y: 250 }, + { persistentUuid: '2', name: 'MyGlobalObject', x: 350, y: 450 }, + ], + }), + ], + }); + + await hotReloader._hotReloadRuntimeGame( + oldProjectData, + newProjectData, + [], + runtimeGame + ); + + const sceneInstances = scene.getInstancesOf('MyObject'); + expect(sceneInstances.length).to.be(1); + expect(sceneInstances[0].getX()).to.be(150); + expect(sceneInstances[0].getY()).to.be(250); + + const globalInstances = scene.getInstancesOf('MyGlobalObject'); + expect(globalInstances.length).to.be(1); + expect(globalInstances[0].getX()).to.be(350); + expect(globalInstances[0].getY()).to.be(450); + }); + it('can change the image of a sprite object of a scene at hot-reload', async () => { const oldProjectData = createProjectData({ layouts: [