Allow to set a full save configuration for variables, game data and scene data

This commit is contained in:
Florian Rival
2025-10-10 17:24:15 +02:00
parent 3a3e0120b8
commit 8f43c2e89b
6 changed files with 604 additions and 77 deletions

View File

@@ -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;
};

View File

@@ -6,6 +6,11 @@ namespace gdjs {
'Save State - Debug'
);
type ArbitrarySaveConfiguration = {
defaultProfilePersistence: 'Persisted' | 'DoNotSave';
persistedInProfiles: Set<string>;
};
export type RestoreRequestOptions = {
profileNames: string[];
clearSceneStack: boolean;
@@ -26,39 +31,100 @@ namespace gdjs {
return `save-${key}`;
};
const excludedVariables: WeakSet<Variable> = new WeakSet();
const variablesSaveConfiguration: WeakMap<
Variable,
ArbitrarySaveConfiguration
> = new WeakMap();
const runtimeSceneDataSaveConfiguration: WeakMap<
RuntimeGame,
Record<string, ArbitrarySaveConfiguration>
> = 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,

View File

@@ -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);
});
});

View File

@@ -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) &&

View File

@@ -45,6 +45,7 @@ declare type GetNetworkSyncDataOptions = {
syncObjectIdentifiers?: boolean;
shouldExcludeVariableFromData?: (variable: Variable) => boolean;
syncAllBehaviors?: boolean;
syncGameVariables?: boolean;
syncSceneTimers?: boolean;
syncOnceTriggers?: boolean;
syncSounds?: boolean;

View File

@@ -4,6 +4,6 @@ declare type SceneSaveState = {
};
declare type GameSaveState = {
gameNetworkSyncData: GameNetworkSyncData;
gameNetworkSyncData: GameNetworkSyncData | null;
layoutNetworkSyncDatas: SceneSaveState[];
};