diff --git a/Extensions/SaveState/JsExtension.js b/Extensions/SaveState/JsExtension.js index f4b93a7d1a..7e89ff97e6 100644 --- a/Extensions/SaveState/JsExtension.js +++ b/Extensions/SaveState/JsExtension.js @@ -248,28 +248,42 @@ module.exports = { extension .addAction( - 'ExcludeVariableFromSaveState', - _('Exclude a variable from save state'), + 'SetVariableSaveConfiguration', + _('Change the save configuration of a variable'), _( - 'Exclude (or re-enable) a scene or global variable from being saved to the save state.' + 'Set if a scene or global variable should be saved in the default save state. Also allow to specify one or more profiles in which the variable should be saved.' ), - _('Exclude variable _PARAM1_ from save state: _PARAM2_'), - _('Save'), + _( + 'Change save configuration of variable _PARAM1_ by saving it in the default save states: _PARAM2_ and in profiles: _PARAM3_' + ), + _('Advanced configuration'), 'res/actions/saveDown.svg', 'res/actions/saveDown.svg' ) .addCodeOnlyParameter('currentScene', '') .addParameter( 'variable', - _('Variable to exclude from save state'), + _('Variable for which configuration should be changed'), '', false ) - .addParameter('yesorno', _('Exclude from save state'), '', false) + .addParameter('yesorno', _('Persist in default save states'), '', false) .setDefaultValue('yes') + .addParameter( + 'string', + _('Profiles in which the variable should be saved'), + '', + true + ) + .setDefaultValue('') + .setParameterLongDescription( + _( + 'Comma-separated list of profile names in which the variable will be saved. When a save state is created with one or more profile names specified, the variable will be saved only if it matches one of these profiles.' + ) + ) .getCodeExtraInformation() .setIncludeFile('Extensions/SaveState/savestatetools.js') - .setFunctionName('gdjs.saveState.excludeVariableFromSaveState'); + .setFunctionName('gdjs.saveState.setVariableSaveConfiguration'); // Save Configuration behavior const saveConfigurationBehavior = new gd.BehaviorJsImplementation(); @@ -319,7 +333,8 @@ module.exports = { _( 'Comma-separated list of profile names in which the object is saved. When a save state is created with one or more profile names specified, the object will be saved only if it matches one of these profiles.' ) - ); + ) + .setAdvanced(true); return behaviorProperties; }; diff --git a/Extensions/SaveState/savestatetools.ts b/Extensions/SaveState/savestatetools.ts index 9266fbaa2c..d06d061246 100644 --- a/Extensions/SaveState/savestatetools.ts +++ b/Extensions/SaveState/savestatetools.ts @@ -6,6 +6,11 @@ namespace gdjs { 'Save State - Debug' ); + type ArbitrarySaveConfiguration = { + defaultProfilePersistence: 'Persisted' | 'DoNotSave'; + persistedInProfiles: Set; + }; + export type RestoreRequestOptions = { profileNames: string[]; clearSceneStack: boolean; @@ -26,39 +31,100 @@ namespace gdjs { return `save-${key}`; }; - const excludedVariables: WeakSet = new WeakSet(); + const variablesSaveConfiguration: WeakMap< + Variable, + ArbitrarySaveConfiguration + > = new WeakMap(); + const runtimeSceneDataSaveConfiguration: WeakMap< + RuntimeGame, + Record + > = new WeakMap(); + const runtimeGameDataSaveConfiguration: WeakMap< + RuntimeGame, + ArbitrarySaveConfiguration + > = new WeakMap(); - export const excludeVariableFromSaveState = ( + export const setVariableSaveConfiguration = ( _: gdjs.RuntimeScene, variable: gdjs.Variable, - exclude: boolean + persistInDefaultProfile: boolean, + persistedInProfilesAsString: string ) => { - if (exclude) { - excludedVariables.add(variable); - } else { - excludedVariables.delete(variable); + variablesSaveConfiguration.set(variable, { + defaultProfilePersistence: persistInDefaultProfile + ? 'Persisted' + : 'DoNotSave', + persistedInProfiles: new Set( + parseCommaSeparatedProfileNames(persistedInProfilesAsString) + ), + }); + }; + + export const setRuntimeSceneDataSaveConfiguration = ( + runtimeScene: gdjs.RuntimeScene, + sceneName: string, + defaultProfilePersistence: 'Persisted' | 'DoNotSave', + persistedInProfilesAsString: string + ) => { + const runtimeSceneDataSaveConfigurations = + runtimeSceneDataSaveConfiguration.get(runtimeScene.getGame()) || {}; + + runtimeSceneDataSaveConfiguration.set(runtimeScene.getGame(), { + ...runtimeSceneDataSaveConfigurations, + [sceneName]: { + defaultProfilePersistence, + persistedInProfiles: new Set( + parseCommaSeparatedProfileNames(persistedInProfilesAsString) + ), + }, + }); + }; + + export const setRuntimeGameDataSaveConfiguration = ( + runtimeScene: gdjs.RuntimeScene, + defaultProfilePersistence: 'Persisted' | 'DoNotSave', + persistedInProfilesAsString: string + ) => { + runtimeGameDataSaveConfiguration.set(runtimeScene.getGame(), { + defaultProfilePersistence, + persistedInProfiles: new Set( + parseCommaSeparatedProfileNames(persistedInProfilesAsString) + ), + }); + }; + + const checkIfIsPersistedInProfiles = ( + profileNames: string[], + configuration: ArbitrarySaveConfiguration | null | undefined + ) => { + if (profileNames.includes('default')) { + if ( + !configuration || + configuration.defaultProfilePersistence === 'Persisted' + ) { + return true; + } } + + if (configuration) { + for (const profileName of profileNames) { + if (configuration.persistedInProfiles.has(profileName)) { + return true; + } + } + } + + return false; }; - export const isVariableExcludedFromSaveState = ( - variable: gdjs.Variable - ) => { - return excludedVariables.has(variable); - }; - - const getNetworkSyncOptions: GetNetworkSyncDataOptions = { - syncObjectIdentifiers: true, - shouldExcludeVariableFromData: isVariableExcludedFromSaveState, - syncAllBehaviors: true, - syncSceneTimers: true, - syncOnceTriggers: true, - syncSounds: true, - syncTweens: true, - syncLayers: true, - syncAsyncTasks: true, - syncSceneVisualProps: true, - syncFullTileMaps: true, - }; + const makeIsVariableExcludedFromSaveState = + (profileNames: string[]) => (variable: gdjs.Variable) => { + const saveConfiguration = variablesSaveConfiguration.get(variable); + return !checkIfIsPersistedInProfiles( + profileNames, + saveConfiguration || null + ); + }; let lastSaveTime: number | null = null; let lastLoadTime: number | null = null; @@ -132,16 +198,40 @@ namespace gdjs { ) => { const { profileNames } = options; + const getNetworkSyncOptions: GetNetworkSyncDataOptions = { + syncObjectIdentifiers: true, + shouldExcludeVariableFromData: + makeIsVariableExcludedFromSaveState(profileNames), + syncAllBehaviors: true, + syncGameVariables: true, + syncSceneTimers: true, + syncOnceTriggers: true, + syncSounds: true, + syncTweens: true, + syncLayers: true, + syncAsyncTasks: true, + syncSceneVisualProps: true, + syncFullTileMaps: true, + }; + + const shouldPersistGameData = checkIfIsPersistedInProfiles( + options.profileNames, + runtimeGameDataSaveConfiguration.get(runtimeGame) + ); + const gameSaveState: GameSaveState = { - gameNetworkSyncData: {}, + // Always persist some game data, but limit it to just the scene stack + // if asked to not persist the game data. + gameNetworkSyncData: runtimeGame.getNetworkSyncData({ + ...getNetworkSyncOptions, + syncGameVariables: shouldPersistGameData, + syncSounds: shouldPersistGameData, + }), layoutNetworkSyncDatas: [], }; - const gameData = runtimeGame.getNetworkSyncData(getNetworkSyncOptions); const scenes = runtimeGame.getSceneStack().getAllScenes(); - gameSaveState.gameNetworkSyncData = gameData || {}; - - scenes.forEach((scene, index) => { + scenes.forEach((runtimeScene, index) => { gameSaveState.layoutNetworkSyncDatas[index] = { sceneData: {} as LayoutNetworkSyncData, objectDatas: {}, @@ -149,7 +239,7 @@ namespace gdjs { // First collect all object sync data, as they may generate unique // identifiers like their networkId. - for (const object of scene.getAdhocListOfAllInstances()) { + for (const object of runtimeScene.getAdhocListOfAllInstances()) { // By default, an object which has no SaveConfiguration behavior is like // it has the default profile persistence set to "Persisted". let shouldPersist = profileNames.includes('default'); @@ -187,11 +277,21 @@ namespace gdjs { } } - // Collect all scene data in the end. - const sceneDatas = (scene.getNetworkSyncData(getNetworkSyncOptions) || - []) as LayoutNetworkSyncData; - - gameSaveState.layoutNetworkSyncDatas[index].sceneData = sceneDatas; + // Collect scene data after the objects: + const shouldPersistSceneData = checkIfIsPersistedInProfiles( + options.profileNames, + (runtimeSceneDataSaveConfiguration.get(runtimeGame) || {})[ + runtimeScene.getName() + ] + ); + if (shouldPersistSceneData) { + const sceneData = runtimeScene.getNetworkSyncData( + getNetworkSyncOptions + ); + if (sceneData) { + gameSaveState.layoutNetworkSyncDatas[index].sceneData = sceneData; + } + } }); return gameSaveState; @@ -204,7 +304,7 @@ namespace gdjs { ) { try { const gameSaveState = createGameSaveState(runtimeScene.getGame(), { - profileNames: parseCommaSeparatedProfileNames( + profileNames: parseCommaSeparatedProfileNamesOrDefault( commaSeparatedProfileNames ), }); @@ -223,7 +323,7 @@ namespace gdjs { ) { try { const gameSaveState = createGameSaveState(runtimeScene.getGame(), { - profileNames: parseCommaSeparatedProfileNames( + profileNames: parseCommaSeparatedProfileNamesOrDefault( commaSeparatedProfileNames ), }); @@ -367,15 +467,30 @@ namespace gdjs { clearInputs: true, keepControl: true, ignoreVariableOwnership: true, - shouldExcludeVariableFromUpdate: isVariableExcludedFromSaveState, + shouldExcludeVariableFromUpdate: makeIsVariableExcludedFromSaveState( + options.profileNames + ), }; // First update the game, which will update the variables, // and set the scene stack to update when ready. - runtimeGame.updateFromNetworkSyncData( - saveState.gameNetworkSyncData, - updateFromNetworkSyncDataOptions - ); + if (saveState.gameNetworkSyncData) { + const shouldRestoreGameData = checkIfIsPersistedInProfiles( + options.profileNames, + runtimeGameDataSaveConfiguration.get(runtimeGame) + ); + + runtimeGame.updateFromNetworkSyncData( + shouldRestoreGameData + ? saveState.gameNetworkSyncData + : { + // Disable game data restoration if asked to, but + // still always keep `ss` (scene stack) restoration as it's always needed. + ss: saveState.gameNetworkSyncData.ss, + }, + updateFromNetworkSyncDataOptions + ); + } // Apply the scene stack updates, as we are at the end of a frame, // we can safely do it. @@ -450,23 +565,42 @@ namespace gdjs { } // Update the rest of the scene last. - runtimeScene.updateFromNetworkSyncData( - layoutSyncData.sceneData, - updateFromNetworkSyncDataOptions - ); + if ( + checkIfIsPersistedInProfiles( + options.profileNames, + (runtimeSceneDataSaveConfiguration.get(runtimeGame) || {})[ + runtimeScene.getName() + ] + ) + ) { + runtimeScene.updateFromNetworkSyncData( + layoutSyncData.sceneData, + updateFromNetworkSyncDataOptions + ); + } }); }; const parseCommaSeparatedProfileNames = ( commaSeparatedProfileNames: string - ): string[] => { - if (!commaSeparatedProfileNames) return ['default']; + ): string[] | null => { + if (!commaSeparatedProfileNames) return null; return commaSeparatedProfileNames .split(',') .map((profileName) => profileName.trim()); }; + const parseCommaSeparatedProfileNamesOrDefault = ( + commaSeparatedProfileNames: string + ): string[] => { + return ( + parseCommaSeparatedProfileNames(commaSeparatedProfileNames) || [ + 'default', + ] + ); + }; + export const restoreGameSaveStateFromVariable = async function ( _: gdjs.RuntimeScene, variable: gdjs.Variable, @@ -478,7 +612,7 @@ namespace gdjs { // and avoid possible conflicts with running events. restoreRequestOptions = { fromVariable: variable, - profileNames: parseCommaSeparatedProfileNames( + profileNames: parseCommaSeparatedProfileNamesOrDefault( commaSeparatedProfileNames ), clearSceneStack, @@ -496,7 +630,7 @@ namespace gdjs { // and avoid possible conflicts with running events. restoreRequestOptions = { fromStorageName: storageName, - profileNames: parseCommaSeparatedProfileNames( + profileNames: parseCommaSeparatedProfileNamesOrDefault( commaSeparatedProfileNames ), clearSceneStack, diff --git a/Extensions/SaveState/tests/savestate.spec.js b/Extensions/SaveState/tests/savestate.spec.js index 1ba8fd9999..c0098028cb 100644 --- a/Extensions/SaveState/tests/savestate.spec.js +++ b/Extensions/SaveState/tests/savestate.spec.js @@ -1,6 +1,6 @@ // @ts-check -describe('SaveState', () => { +describe.only('SaveState', () => { /** * @param {{name: string, x: number, y: number}} content * @returns {InstanceData} @@ -443,10 +443,11 @@ describe('SaveState', () => { variable3.setNumber(42); runtimeScene1.getVariables().add('Variable3', variable3); - gdjs.saveState.excludeVariableFromSaveState( + gdjs.saveState.setVariableSaveConfiguration( runtimeScene1, variable3, - true + false, + '' ); // Create some objects in addition to initial objects at specific positions. @@ -847,5 +848,376 @@ describe('SaveState', () => { expect(restoredProfile2Objects[0].getX()).to.be(301); expect(restoredProfile2Objects[0].getY()).to.be(401); }); + + it('saves a running game (only game/scene data in the specified profiles)', async () => { + // Start a game with objects configured for different profiles. + const scene1Data = getFakeSceneData({ + name: 'Scene1', + }); + const scene2Data = getFakeSceneData({ + name: 'Scene2', + }); + + const runtimeGame1 = gdjs.getPixiRuntimeGame({ + layouts: [scene1Data, scene2Data], + }); + await runtimeGame1._resourcesLoader.loadAllResources(() => {}); + + const runtimeScene1 = runtimeGame1.getSceneStack().push({ + sceneName: 'Scene1', + }); + if (!runtimeScene1) throw new Error('No current scene was created.'); + + const scene1Variable1 = new gdjs.Variable(); + scene1Variable1.setString('Scene1Variable1TestValue'); + runtimeScene1.getVariables().add('Scene1Variable1', scene1Variable1); + const scene1Variable2 = new gdjs.Variable(); + scene1Variable2.setString('Scene1Variable2TestValue'); + runtimeScene1.getVariables().add('Scene1Variable2', scene1Variable2); + gdjs.saveState.setVariableSaveConfiguration( + runtimeScene1, + scene1Variable2, + false, + 'profile1' + ); + + const runtimeScene2 = runtimeGame1.getSceneStack().push({ + sceneName: 'Scene2', + }); + if (!runtimeScene2) throw new Error('No current scene was created.'); + + const scene2Variable1 = new gdjs.Variable(); + scene2Variable1.setString('Scene2Variable1TestValue'); + runtimeScene2.getVariables().add('Scene2Variable1', scene2Variable1); + + gdjs.saveState.setRuntimeSceneDataSaveConfiguration( + runtimeScene1, + 'Scene1', + true, + 'profile1' + ); + gdjs.saveState.setRuntimeSceneDataSaveConfiguration( + runtimeScene2, + 'Scene2', + false, + 'profile2' + ); + gdjs.saveState.setRuntimeGameDataSaveConfiguration( + runtimeScene1, + false, + 'game-only' + ); + + // Save the game state with the different profiles. + const saveStateProfile1 = gdjs.saveState.createGameSaveState( + runtimeGame1, + { + profileNames: ['profile1'], + } + ); + const saveStateProfile2 = gdjs.saveState.createGameSaveState( + runtimeGame1, + { + profileNames: ['profile2'], + } + ); + const saveStateGameOnly = gdjs.saveState.createGameSaveState( + runtimeGame1, + { + profileNames: ['game-only'], + } + ); + + // First save state "profile1" should save the first scene data, notably variables: + expect(saveStateProfile1.gameNetworkSyncData.var).to.be(undefined); + expect(saveStateProfile1.gameNetworkSyncData.extVar).to.be(undefined); + expect( + (saveStateProfile1.gameNetworkSyncData.ss || []).map(({ name }) => name) + ).to.eql(['Scene1', 'Scene2']); + expect(saveStateProfile1.layoutNetworkSyncDatas[0].sceneData.var).to.eql([ + { + name: 'Scene1Variable2', + value: 'Scene1Variable2TestValue', + type: 'string', + children: undefined, + owner: 0, + }, + ]); + expect(saveStateProfile1.layoutNetworkSyncDatas[1].sceneData.var).to.be( + undefined + ); + + // Second save state "profile2" should save the second scene data only: + expect(saveStateProfile2.gameNetworkSyncData.var).to.be(undefined); + expect(saveStateProfile2.gameNetworkSyncData.extVar).to.be(undefined); + expect( + (saveStateProfile2.gameNetworkSyncData.ss || []).map(({ name }) => name) + ).to.eql(['Scene1', 'Scene2']); + expect(saveStateProfile2.layoutNetworkSyncDatas[0].sceneData.var).to.be( + undefined + ); + expect( + saveStateProfile2.layoutNetworkSyncDatas[1].sceneData.var + ).not.to.be(undefined); + + // Third save state "game-only" should save the game data only: + expect(saveStateGameOnly.gameNetworkSyncData.var).not.to.be(undefined); + expect(saveStateGameOnly.gameNetworkSyncData.extVar).not.to.be(undefined); + expect( + (saveStateGameOnly.gameNetworkSyncData.ss || []).map(({ name }) => name) + ).to.eql(['Scene1', 'Scene2']); + + console.log(JSON.stringify(saveStateGameOnly, null, 2)); + }); + }); + + it('loads a running game (only game/scene data in the specified profiles)', async () => { + // Start a game with objects configured for different profiles. + const scene1Data = getFakeSceneData({ + name: 'Scene1', + }); + const scene2Data = getFakeSceneData({ + name: 'Scene2', + }); + + const runtimeGame1 = gdjs.getPixiRuntimeGame({ + layouts: [scene1Data, scene2Data], + }); + await runtimeGame1._resourcesLoader.loadAllResources(() => {}); + + const runtimeScene1 = runtimeGame1.getSceneStack().push({ + sceneName: 'Scene1', + }); + if (!runtimeScene1) throw new Error('No current scene was created.'); + + const scene1Variable1 = new gdjs.Variable(); + scene1Variable1.setString('Scene1Variable1TestValue'); + runtimeScene1.getVariables().add('Scene1Variable1', scene1Variable1); + const scene1Variable2 = new gdjs.Variable(); + scene1Variable2.setString('Scene1Variable2TestValue'); + runtimeScene1.getVariables().add('Scene1Variable2', scene1Variable2); + gdjs.saveState.setVariableSaveConfiguration( + runtimeScene1, + scene1Variable2, + false, + 'profile1' + ); + + const runtimeScene2 = runtimeGame1.getSceneStack().push({ + sceneName: 'Scene2', + }); + if (!runtimeScene2) throw new Error('No current scene was created.'); + + const scene2Variable1 = new gdjs.Variable(); + scene2Variable1.setString('Scene2Variable1TestValue'); + runtimeScene2.getVariables().add('Scene2Variable1', scene2Variable1); + gdjs.saveState.setVariableSaveConfiguration( + runtimeScene2, + scene2Variable1, + false, + 'profile2' + ); + + // Modify the global volume so that it's different from the one saved in the save state: + runtimeGame1.getSoundManager().setGlobalVolume(33); + + // Set what belongs to each profile: + gdjs.saveState.setRuntimeSceneDataSaveConfiguration( + runtimeScene1, + 'Scene1', + true, + 'profile1' + ); + gdjs.saveState.setRuntimeSceneDataSaveConfiguration( + runtimeScene2, + 'Scene2', + false, + 'profile2' + ); + gdjs.saveState.setRuntimeGameDataSaveConfiguration( + runtimeScene1, + false, + 'game-only' + ); + + /** @type {GameSaveState} */ + const completeSaveState = { + gameNetworkSyncData: { + ss: [ + { + name: 'Scene1', + networkId: 'b68fda7c', + }, + { + name: 'Scene2', + networkId: '406dafce', + }, + ], + "sm": { + "globalVolume": 75, + "cachedSpatialPosition": {}, + "freeMusics": [], + "freeSounds": [], + "musics": {}, + "sounds": {} + }, + }, + layoutNetworkSyncDatas: [ + { + sceneData: { + var: [ + { + name: 'Scene1Variable2', + value: 'some-loaded-value', + type: 'string', + owner: 0, + }, + ], + extVar: {}, + id: 'b68fda7c', + color: 0, + layers: { + '': { + timeScale: 1, + defaultZOrder: 0, + hidden: false, + effects: {}, + followBaseLayerCamera: true, + clearColor: [0, 0, 0, 1], + cameraX: 400, + cameraY: 300, + cameraZ: 0, + cameraRotation: 0, + cameraZoom: 1, + }, + }, + time: { + elapsedTime: 0, + timeScale: 1, + timeFromStart: 0, + firstFrame: true, + timers: { + items: {}, + }, + firstUpdateDone: false, + }, + once: { + onceTriggers: {}, + lastFrameOnceTriggers: {}, + }, + tween: { + tweens: {}, + }, + async: { + tasks: [], + }, + }, + objectDatas: {}, + }, + { + sceneData: { + var: [ + { + name: 'Scene2Variable1', + value: 'some-other-loaded-value', + type: 'string', + owner: 0, + }, + ], + extVar: {}, + id: '406dafce', + color: 0, + layers: { + '': { + timeScale: 1, + defaultZOrder: 0, + hidden: false, + effects: {}, + followBaseLayerCamera: true, + clearColor: [0, 0, 0, 1], + cameraX: 400, + cameraY: 300, + cameraZ: 0, + cameraRotation: 0, + cameraZoom: 1, + }, + }, + time: { + elapsedTime: 0, + timeScale: 1, + timeFromStart: 0, + firstFrame: true, + timers: { + items: {}, + }, + firstUpdateDone: false, + }, + once: { + onceTriggers: {}, + lastFrameOnceTriggers: {}, + }, + tween: { + tweens: {}, + }, + async: { + tasks: [], + }, + }, + objectDatas: {}, + }, + ], + }; + + // Restore only the profile1 data: + gdjs.saveState.restoreGameSaveState( + runtimeGame1, + completeSaveState, + { + profileNames: ['profile1'], + clearSceneStack: false, + } + ); + + // Check scene 1 data was restored: + expect(runtimeScene1.getVariables().get('Scene1Variable1').getAsString()).to.be( + 'Scene1Variable1TestValue' // Unchanged (not part of the profile) + ); + expect(runtimeScene1.getVariables().get('Scene1Variable2').getAsString()).to.be( + 'some-loaded-value' // Updated (part of the profile) + ); + // Scene 2 data was not restored, nor the game data: + expect(runtimeScene2.getVariables().get('Scene2Variable1').getAsString()).to.be( + 'Scene2Variable1TestValue' + ); + expect(runtimeGame1.getSoundManager().getGlobalVolume()).to.be(33); + + // Now, restore the profile2 data: + gdjs.saveState.restoreGameSaveState( + runtimeGame1, + completeSaveState, + { + profileNames: ['profile2'], + clearSceneStack: false, + } + ); + + // Scene 2 data was restored: + expect(runtimeScene2.getVariables().get('Scene2Variable1').getAsString()).to.be( + 'some-other-loaded-value' + ); + // But not the game data: + expect(runtimeGame1.getSoundManager().getGlobalVolume()).to.be(33); + + // Finally, restore the "game-only" data: + gdjs.saveState.restoreGameSaveState( + runtimeGame1, + completeSaveState, + { + profileNames: ['game-only'], + } + ); + + // Game data was restored: + expect(runtimeGame1.getSoundManager().getGlobalVolume()).to.be(75); }); }); diff --git a/GDJS/Runtime/runtimegame.ts b/GDJS/Runtime/runtimegame.ts index d4b7403377..9778d5069f 100644 --- a/GDJS/Runtime/runtimegame.ts +++ b/GDJS/Runtime/runtimegame.ts @@ -1384,24 +1384,29 @@ namespace gdjs { syncOptions: GetNetworkSyncDataOptions ): GameNetworkSyncData | null { const syncData: GameNetworkSyncData = { - var: this._variables.getNetworkSyncData(syncOptions), + var: + syncOptions.syncGameVariables === false + ? undefined + : this._variables.getNetworkSyncData(syncOptions), sm: syncOptions.syncSounds ? this.getSoundManager().getNetworkSyncData() : undefined, ss: this._sceneStack.getNetworkSyncData(syncOptions) || undefined, }; - const extensionsVariablesSyncData = {}; - this._variablesByExtensionName.forEach((variables, extensionName) => { - const extensionVariablesSyncData = - variables.getNetworkSyncData(syncOptions); - // If there is no variables to sync, don't include the extension in the sync data. - if (extensionVariablesSyncData.length) { - extensionsVariablesSyncData[extensionName] = - extensionVariablesSyncData; - } - }); - syncData.extVar = extensionsVariablesSyncData; + if (syncOptions.syncGameVariables !== false) { + const extensionsVariablesSyncData = {}; + this._variablesByExtensionName.forEach((variables, extensionName) => { + const extensionVariablesSyncData = + variables.getNetworkSyncData(syncOptions); + // If there is no variables to sync, don't include the extension in the sync data. + if (extensionVariablesSyncData.length) { + extensionsVariablesSyncData[extensionName] = + extensionVariablesSyncData; + } + }); + syncData.extVar = extensionsVariablesSyncData; + } if ( (!syncData.var || syncData.var.length === 0) && diff --git a/GDJS/Runtime/types/project-data.d.ts b/GDJS/Runtime/types/project-data.d.ts index ff5e982cb7..79bf831ce4 100644 --- a/GDJS/Runtime/types/project-data.d.ts +++ b/GDJS/Runtime/types/project-data.d.ts @@ -45,6 +45,7 @@ declare type GetNetworkSyncDataOptions = { syncObjectIdentifiers?: boolean; shouldExcludeVariableFromData?: (variable: Variable) => boolean; syncAllBehaviors?: boolean; + syncGameVariables?: boolean; syncSceneTimers?: boolean; syncOnceTriggers?: boolean; syncSounds?: boolean; diff --git a/GDJS/Runtime/types/save-state.d.ts b/GDJS/Runtime/types/save-state.d.ts index cbab19f0f3..b8dd873c9d 100644 --- a/GDJS/Runtime/types/save-state.d.ts +++ b/GDJS/Runtime/types/save-state.d.ts @@ -4,6 +4,6 @@ declare type SceneSaveState = { }; declare type GameSaveState = { - gameNetworkSyncData: GameNetworkSyncData; + gameNetworkSyncData: GameNetworkSyncData | null; layoutNetworkSyncDatas: SceneSaveState[]; };