mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Allow to set a full save configuration for variables, game data and scene data
This commit is contained in:
@@ -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;
|
||||
};
|
||||
|
@@ -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,
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
|
@@ -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) &&
|
||||
|
1
GDJS/Runtime/types/project-data.d.ts
vendored
1
GDJS/Runtime/types/project-data.d.ts
vendored
@@ -45,6 +45,7 @@ declare type GetNetworkSyncDataOptions = {
|
||||
syncObjectIdentifiers?: boolean;
|
||||
shouldExcludeVariableFromData?: (variable: Variable) => boolean;
|
||||
syncAllBehaviors?: boolean;
|
||||
syncGameVariables?: boolean;
|
||||
syncSceneTimers?: boolean;
|
||||
syncOnceTriggers?: boolean;
|
||||
syncSounds?: boolean;
|
||||
|
2
GDJS/Runtime/types/save-state.d.ts
vendored
2
GDJS/Runtime/types/save-state.d.ts
vendored
@@ -4,6 +4,6 @@ declare type SceneSaveState = {
|
||||
};
|
||||
|
||||
declare type GameSaveState = {
|
||||
gameNetworkSyncData: GameNetworkSyncData;
|
||||
gameNetworkSyncData: GameNetworkSyncData | null;
|
||||
layoutNetworkSyncDatas: SceneSaveState[];
|
||||
};
|
||||
|
Reference in New Issue
Block a user