mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
1224 lines
40 KiB
JavaScript
1224 lines
40 KiB
JavaScript
// @ts-check
|
|
|
|
describe.only('SaveState', () => {
|
|
/**
|
|
* @param {{name: string, x: number, y: number}} content
|
|
* @returns {InstanceData}
|
|
*/
|
|
const getFakeInstanceData = ({ name, x, y }) => ({
|
|
persistentUuid: '',
|
|
|
|
layer: '',
|
|
locked: false,
|
|
name,
|
|
|
|
x,
|
|
y,
|
|
|
|
angle: 0,
|
|
|
|
zOrder: 0,
|
|
|
|
customSize: false,
|
|
width: 0,
|
|
height: 0,
|
|
depth: 0,
|
|
|
|
numberProperties: [],
|
|
stringProperties: [],
|
|
initialVariables: [],
|
|
});
|
|
|
|
/**
|
|
* @param {{name: string, objects?: gdjs.SpriteObjectData[], instances?: InstanceData[]}} settings
|
|
* @returns {LayoutData}
|
|
*/
|
|
const getFakeSceneData = ({ name, objects, instances }) => ({
|
|
layers: [
|
|
{
|
|
name: '',
|
|
visibility: true,
|
|
effects: [],
|
|
cameras: [],
|
|
ambientLightColorR: 0,
|
|
ambientLightColorG: 0,
|
|
ambientLightColorB: 0,
|
|
isLightingLayer: false,
|
|
followBaseLayerCamera: true,
|
|
},
|
|
],
|
|
r: 0,
|
|
v: 0,
|
|
b: 0,
|
|
mangledName: name,
|
|
name: name,
|
|
stopSoundsOnStartup: false,
|
|
title: '',
|
|
behaviorsSharedData: [],
|
|
objects: objects || [
|
|
// @ts-ignore - This is a gdjs.SpriteObjectData.
|
|
{
|
|
type: 'Sprite',
|
|
name: 'MySpriteObject',
|
|
behaviors: [],
|
|
effects: [],
|
|
variables: [],
|
|
|
|
animations: [],
|
|
updateIfNotVisible: false,
|
|
},
|
|
],
|
|
instances: instances || [],
|
|
variables: [],
|
|
usedResources: [],
|
|
});
|
|
|
|
describe('Save State Basics', () => {
|
|
it('saves and restores a game with objects at specific positions (without SaveConfiguration behavior)', async () => {
|
|
// Start a game.
|
|
const runtimeGame1 = gdjs.getPixiRuntimeGame({
|
|
layouts: [getFakeSceneData({ name: 'Scene1' })],
|
|
});
|
|
await runtimeGame1._resourcesLoader.loadAllResources(() => {});
|
|
|
|
const runtimeScene1 = runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene1',
|
|
});
|
|
if (!runtimeScene1) throw new Error('No current scene was created.');
|
|
|
|
// Create some objects at specific positions.
|
|
const object1 = runtimeScene1.createObject('MySpriteObject');
|
|
const object2 = runtimeScene1.createObject('MySpriteObject');
|
|
|
|
if (!object1 || !object2) {
|
|
throw new Error('Objects were not created');
|
|
}
|
|
|
|
object1.setX(100);
|
|
object1.setY(200);
|
|
object2.setX(300);
|
|
object2.setY(400);
|
|
|
|
const object1Id = object1.id;
|
|
const object2Id = object2.id;
|
|
|
|
// Save the game state.
|
|
const saveState = gdjs.saveState.createGameSaveState(runtimeGame1, {
|
|
profileNames: ['default'],
|
|
});
|
|
|
|
expect(saveState).not.to.be(null);
|
|
expect(saveState.layoutNetworkSyncDatas).to.have.length(1);
|
|
expect(
|
|
saveState.layoutNetworkSyncDatas[0].objectDatas[object1Id]
|
|
).not.to.be(undefined);
|
|
expect(
|
|
saveState.layoutNetworkSyncDatas[0].objectDatas[object2Id]
|
|
).not.to.be(undefined);
|
|
|
|
// Start a new game.
|
|
const runtimeGame2 = gdjs.getPixiRuntimeGame({
|
|
layouts: [getFakeSceneData({ name: 'Scene1' })],
|
|
});
|
|
await runtimeGame2._resourcesLoader.loadAllResources(() => {});
|
|
|
|
// Load the saved state.
|
|
gdjs.saveState.restoreGameSaveState(runtimeGame2, saveState, {
|
|
profileNames: ['default'],
|
|
});
|
|
|
|
const runtimeScene2 = runtimeGame2.getSceneStack().getCurrentScene();
|
|
if (!runtimeScene2) throw new Error('No current scene was restored.');
|
|
|
|
// Verify objects are restored.
|
|
const restoredObjects = runtimeScene2.getObjects('MySpriteObject');
|
|
expect(restoredObjects.length).to.be(2);
|
|
|
|
// Find objects by their positions (since IDs might be different).
|
|
const restoredObject1 = restoredObjects.find(
|
|
(obj) => obj.getX() === 100 && obj.getY() === 200
|
|
);
|
|
const restoredObject2 = restoredObjects.find(
|
|
(obj) => obj.getX() === 300 && obj.getY() === 400
|
|
);
|
|
if (!restoredObject1 || !restoredObject2) {
|
|
throw new Error(
|
|
'Objects not found at the proper positions after restore.'
|
|
);
|
|
}
|
|
|
|
expect(restoredObject1.getName()).to.be('MySpriteObject');
|
|
expect(restoredObject2.getName()).to.be('MySpriteObject');
|
|
});
|
|
|
|
it('saves and restores scene variables', async () => {
|
|
// Start a game.
|
|
const runtimeGame1 = gdjs.getPixiRuntimeGame({
|
|
layouts: [getFakeSceneData({ name: 'Scene1' })],
|
|
});
|
|
await runtimeGame1._resourcesLoader.loadAllResources(() => {});
|
|
|
|
const runtimeScene1 = runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene1',
|
|
});
|
|
if (!runtimeScene1) throw new Error('No current scene was created.');
|
|
|
|
// Create scene variables.
|
|
const sceneVariable = new gdjs.Variable();
|
|
sceneVariable.setString('TestValue');
|
|
runtimeScene1.getVariables().add('MyVariable', sceneVariable);
|
|
|
|
// Save the game state.
|
|
const saveState = gdjs.saveState.createGameSaveState(runtimeGame1, {
|
|
profileNames: ['default'],
|
|
});
|
|
|
|
// Start a new game.
|
|
const runtimeGame2 = gdjs.getPixiRuntimeGame({
|
|
layouts: [getFakeSceneData({ name: 'Scene1' })],
|
|
});
|
|
await runtimeGame2._resourcesLoader.loadAllResources(() => {});
|
|
|
|
// Load the saved state.
|
|
gdjs.saveState.restoreGameSaveState(runtimeGame2, saveState, {
|
|
profileNames: ['default'],
|
|
});
|
|
|
|
const runtimeScene2 = runtimeGame2.getSceneStack().getCurrentScene();
|
|
if (!runtimeScene2) throw new Error('No current scene was restored.');
|
|
|
|
// Verify variable is restored.
|
|
expect(runtimeScene2.getVariables().has('MyVariable')).to.be(true);
|
|
expect(
|
|
runtimeScene2.getVariables().get('MyVariable').getAsString()
|
|
).to.be('TestValue');
|
|
});
|
|
|
|
it('saves and restores global variables', async () => {
|
|
// Start a game.
|
|
const runtimeGame1 = gdjs.getPixiRuntimeGame({
|
|
layouts: [getFakeSceneData({ name: 'Scene1' })],
|
|
});
|
|
await runtimeGame1._resourcesLoader.loadAllResources(() => {});
|
|
|
|
const runtimeScene1 = runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene1',
|
|
});
|
|
if (!runtimeScene1) throw new Error('No current scene was created.');
|
|
|
|
// Create global variables
|
|
const globalVariable = new gdjs.Variable();
|
|
globalVariable.setNumber(42);
|
|
runtimeGame1.getVariables().add('MyGlobalVariable', globalVariable);
|
|
|
|
// Save the game state
|
|
const saveState = gdjs.saveState.createGameSaveState(runtimeGame1, {
|
|
profileNames: ['default'],
|
|
});
|
|
|
|
// Start a new game.
|
|
const runtimeGame2 = gdjs.getPixiRuntimeGame({
|
|
layouts: [getFakeSceneData({ name: 'Scene1' })],
|
|
});
|
|
await runtimeGame2._resourcesLoader.loadAllResources(() => {});
|
|
|
|
// Load the saved state.
|
|
gdjs.saveState.restoreGameSaveState(runtimeGame2, saveState, {
|
|
profileNames: ['default'],
|
|
});
|
|
|
|
const runtimeScene2 = runtimeGame2.getSceneStack().getCurrentScene();
|
|
if (!runtimeScene2) throw new Error('No current scene was restored.');
|
|
|
|
// Verify global variable is restored.
|
|
expect(runtimeGame2.getVariables().has('MyGlobalVariable')).to.be(true);
|
|
expect(
|
|
runtimeGame2.getVariables().get('MyGlobalVariable').getAsNumber()
|
|
).to.be(42);
|
|
});
|
|
});
|
|
|
|
describe('Save State with initial instances and SaveConfiguration behavior', () => {
|
|
it('saves and restores with initial instances and objects having the SaveConfiguration behavior', async () => {
|
|
// Start a game.
|
|
const sceneData = getFakeSceneData({
|
|
name: 'Scene1',
|
|
objects: [
|
|
{
|
|
type: 'Sprite',
|
|
name: 'NotSavedSpriteObject',
|
|
behaviors: [
|
|
{
|
|
name: 'SaveConfiguration',
|
|
type: 'SaveState::SaveConfiguration',
|
|
defaultProfilePersistence: 'DoNotSave',
|
|
},
|
|
],
|
|
effects: [],
|
|
variables: [],
|
|
|
|
updateIfNotVisible: false,
|
|
animations: [],
|
|
},
|
|
{
|
|
type: 'Sprite',
|
|
name: 'SavedSpriteObject',
|
|
behaviors: [
|
|
{
|
|
name: 'SaveConfiguration',
|
|
type: 'SaveState::SaveConfiguration',
|
|
defaultProfilePersistence: 'Persisted',
|
|
},
|
|
],
|
|
effects: [],
|
|
variables: [],
|
|
|
|
updateIfNotVisible: false,
|
|
animations: [],
|
|
},
|
|
],
|
|
instances: [
|
|
// A default instance which will be not removed, because the associated object is marked as not persisted.
|
|
getFakeInstanceData({
|
|
name: 'NotSavedSpriteObject',
|
|
x: 100,
|
|
y: 200,
|
|
}),
|
|
// A default instance which will be removed, because the associated object is marked as persisted.
|
|
getFakeInstanceData({
|
|
name: 'SavedSpriteObject',
|
|
x: 300,
|
|
y: 400,
|
|
}),
|
|
],
|
|
});
|
|
const runtimeGame1 = gdjs.getPixiRuntimeGame({
|
|
layouts: [sceneData],
|
|
});
|
|
await runtimeGame1._resourcesLoader.loadAllResources(() => {});
|
|
|
|
const runtimeScene1 = runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene1',
|
|
});
|
|
if (!runtimeScene1) throw new Error('No current scene was created.');
|
|
|
|
// Create some objects in addition to initial objects at specific positions.
|
|
const object3 = runtimeScene1.createObject('SavedSpriteObject');
|
|
const object4 = runtimeScene1.createObject('NotSavedSpriteObject');
|
|
|
|
if (!object3 || !object4) {
|
|
throw new Error('Objects were not created');
|
|
}
|
|
|
|
object3.setX(500);
|
|
object3.setY(600);
|
|
object4.setX(700);
|
|
object4.setY(800);
|
|
|
|
// Save the game state.
|
|
const saveState = gdjs.saveState.createGameSaveState(runtimeGame1, {
|
|
profileNames: ['default'],
|
|
});
|
|
|
|
// Start a new game.
|
|
const runtimeGame2 = gdjs.getPixiRuntimeGame({
|
|
layouts: [sceneData],
|
|
});
|
|
await runtimeGame2._resourcesLoader.loadAllResources(() => {});
|
|
|
|
// Load the saved state.
|
|
gdjs.saveState.restoreGameSaveState(runtimeGame2, saveState, {
|
|
profileNames: ['default'],
|
|
});
|
|
|
|
const runtimeScene2 = runtimeGame2.getSceneStack().getCurrentScene();
|
|
if (!runtimeScene2) throw new Error('No current scene was restored.');
|
|
|
|
// Verify objects are restored: only 2 "SavedSpriteObject" objects should exist
|
|
// after being restored (because the initial instance should be never created,
|
|
// and the 2 from the saved state should be created).
|
|
const restoredSavedObjects =
|
|
runtimeScene2.getObjects('SavedSpriteObject');
|
|
expect(restoredSavedObjects.length).to.be(2);
|
|
|
|
// Verify that the initial instance of "NotSavedSpriteObject" is properly created.
|
|
const notSavedObjects = runtimeScene2.getObjects('NotSavedSpriteObject');
|
|
expect(notSavedObjects.length).to.be(1);
|
|
|
|
// Find objects by their positions (since IDs might be different).
|
|
const restoredObject1 = restoredSavedObjects.find(
|
|
(obj) => obj.getX() === 300 && obj.getY() === 400
|
|
);
|
|
const restoredObject2 = restoredSavedObjects.find(
|
|
(obj) => obj.getX() === 500 && obj.getY() === 600
|
|
);
|
|
const notSavedObject1 = notSavedObjects.find(
|
|
(obj) => obj.getX() === 100 && obj.getY() === 200
|
|
);
|
|
|
|
if (!restoredObject1 || !restoredObject2 || !notSavedObject1) {
|
|
throw new Error(
|
|
'Objects not found at the proper positions after restore.'
|
|
);
|
|
}
|
|
|
|
expect(restoredObject1.getName()).to.be('SavedSpriteObject');
|
|
expect(restoredObject2.getName()).to.be('SavedSpriteObject');
|
|
expect(notSavedObject1.getName()).to.be('NotSavedSpriteObject');
|
|
});
|
|
});
|
|
|
|
describe('Save State restored without clearing the running scenes', () => {
|
|
it('saves and restores the same running game (keep instances, keep variables)', async () => {
|
|
// Start a game.
|
|
const sceneData = getFakeSceneData({
|
|
name: 'Scene1',
|
|
objects: [
|
|
{
|
|
type: 'Sprite',
|
|
name: 'NotSavedSpriteObject',
|
|
behaviors: [
|
|
{
|
|
name: 'SaveConfiguration',
|
|
type: 'SaveState::SaveConfiguration',
|
|
defaultProfilePersistence: 'DoNotSave',
|
|
},
|
|
],
|
|
effects: [],
|
|
variables: [],
|
|
|
|
updateIfNotVisible: false,
|
|
animations: [],
|
|
},
|
|
{
|
|
type: 'Sprite',
|
|
name: 'SavedSpriteObject',
|
|
behaviors: [
|
|
{
|
|
name: 'SaveConfiguration',
|
|
type: 'SaveState::SaveConfiguration',
|
|
defaultProfilePersistence: 'Persisted',
|
|
},
|
|
],
|
|
effects: [],
|
|
variables: [],
|
|
|
|
updateIfNotVisible: false,
|
|
animations: [],
|
|
},
|
|
],
|
|
instances: [
|
|
// A default instance which will be not removed, because the associated object is marked as not persisted.
|
|
getFakeInstanceData({
|
|
name: 'NotSavedSpriteObject',
|
|
x: 100,
|
|
y: 200,
|
|
}),
|
|
// A default instance which will be removed, because the associated object is marked as persisted.
|
|
getFakeInstanceData({
|
|
name: 'SavedSpriteObject',
|
|
x: 300,
|
|
y: 400,
|
|
}),
|
|
],
|
|
});
|
|
const runtimeGame1 = gdjs.getPixiRuntimeGame({
|
|
layouts: [sceneData],
|
|
});
|
|
await runtimeGame1._resourcesLoader.loadAllResources(() => {});
|
|
|
|
const runtimeScene1 = runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene1',
|
|
});
|
|
if (!runtimeScene1) throw new Error('No current scene was created.');
|
|
|
|
// Set some variables.
|
|
const variable1 = new gdjs.Variable();
|
|
variable1.setString('TestValue');
|
|
runtimeScene1.getVariables().add('Variable1', variable1);
|
|
const variable2 = new gdjs.Variable();
|
|
variable2.setBoolean(true);
|
|
runtimeScene1.getVariables().add('Variable2', variable2);
|
|
const variable3 = new gdjs.Variable();
|
|
variable3.setNumber(42);
|
|
runtimeScene1.getVariables().add('Variable3', variable3);
|
|
|
|
gdjs.saveState.setVariableSaveConfiguration(
|
|
runtimeScene1,
|
|
variable3,
|
|
false,
|
|
''
|
|
);
|
|
|
|
// Create some objects in addition to initial objects at specific positions.
|
|
const object3 = runtimeScene1.createObject('SavedSpriteObject');
|
|
const object4 = runtimeScene1.createObject('NotSavedSpriteObject');
|
|
const object5 = runtimeScene1.createObject('SavedSpriteObject');
|
|
|
|
if (!object3 || !object4 || !object5) {
|
|
throw new Error('Objects were not created');
|
|
}
|
|
|
|
object3.setX(500);
|
|
object3.setY(600);
|
|
object4.setX(700);
|
|
object4.setY(800);
|
|
object5.setX(900);
|
|
object5.setY(1000);
|
|
|
|
// Save the game state.
|
|
const saveState = gdjs.saveState.createGameSaveState(runtimeGame1, {
|
|
profileNames: ['default'],
|
|
});
|
|
|
|
// Now, modify the game.
|
|
|
|
// Modify the variables.
|
|
variable1.setString('TestValue2');
|
|
variable2.setBoolean(false);
|
|
variable3.setNumber(43);
|
|
|
|
// Move instances (both saved/unsaved, and created dynamically or from initial instances),
|
|
// to then verify that they are restored at the proper positions (only for the saved ones).
|
|
const object1 = runtimeScene1.getObjects('NotSavedSpriteObject')[0];
|
|
const object2 = runtimeScene1.getObjects('SavedSpriteObject')[0];
|
|
if (!object1 || !object2) {
|
|
throw new Error('Objects created from initial instances not found.');
|
|
}
|
|
object1.setX(101);
|
|
object1.setY(201);
|
|
object2.setX(301);
|
|
object2.setY(401);
|
|
object3.setX(502);
|
|
object3.setY(602);
|
|
object4.setX(701);
|
|
object4.setY(801);
|
|
|
|
// Remember IDs to verify if restored objects are the same objects already living.
|
|
const object2Id = object2.id;
|
|
const object3Id = object3.id;
|
|
const object5Id = object5.id;
|
|
|
|
// Delete an instance (that was saved and should be restored).
|
|
object5.deleteFromScene();
|
|
|
|
// Create new instances (the saved ones should be removed after restoring the save state).
|
|
const object6 = runtimeScene1.createObject('SavedSpriteObject');
|
|
if (!object6) throw new Error('Object not created.');
|
|
object6.setX(1100);
|
|
object6.setY(1200);
|
|
const object7 = runtimeScene1.createObject('NotSavedSpriteObject');
|
|
if (!object7) throw new Error('Object not created.');
|
|
object7.setX(1300);
|
|
object7.setY(1400);
|
|
const object8 = runtimeScene1.createObject('SavedSpriteObject');
|
|
if (!object8) throw new Error('Object not created.');
|
|
object8.setX(1500);
|
|
object8.setY(1600);
|
|
|
|
const object6Id = object6.id;
|
|
|
|
// Render a frame to be sure the deleted object is removed.
|
|
runtimeScene1.renderAndStep(1000 / 60);
|
|
|
|
// Load the saved state on the same game.
|
|
gdjs.saveState.restoreGameSaveState(runtimeGame1, saveState, {
|
|
profileNames: ['default'],
|
|
clearSceneStack: false,
|
|
});
|
|
|
|
const restoredSavedObjects =
|
|
runtimeScene1.getObjects('SavedSpriteObject');
|
|
|
|
expect(
|
|
restoredSavedObjects.map((obj) => ({
|
|
x: obj.getX(),
|
|
y: obj.getY(),
|
|
name: obj.getName(),
|
|
}))
|
|
).to.eql([
|
|
// The initial instance should be restored ("object2").
|
|
{ x: 300, y: 400, name: 'SavedSpriteObject' },
|
|
// The instance created dynamically before the save should be restored ("object3").
|
|
{ x: 500, y: 600, name: 'SavedSpriteObject' },
|
|
// "object5", which was deleted after the save, should be restored.
|
|
{ x: 900, y: 1000, name: 'SavedSpriteObject' },
|
|
// "object6", which was created dynamically after the save, must have been removed.
|
|
]);
|
|
|
|
// Check that the "restored objects" are exactly the same objects already living,
|
|
// and that the restored object is a entirely new one.
|
|
expect(restoredSavedObjects[0].id).to.be(object2Id);
|
|
expect(restoredSavedObjects[1].id).to.be(object3Id);
|
|
expect(restoredSavedObjects[0]).to.be(object2);
|
|
expect(restoredSavedObjects[1]).to.be(object3);
|
|
// This one should be entirely new:
|
|
expect(restoredSavedObjects[2].id).not.to.be(object5Id);
|
|
expect(restoredSavedObjects[2].id).not.to.be(object6Id);
|
|
|
|
const restoredNotSavedObjects = runtimeScene1.getObjects(
|
|
'NotSavedSpriteObject'
|
|
);
|
|
|
|
expect(
|
|
restoredNotSavedObjects.map((obj) => ({
|
|
x: obj.getX(),
|
|
y: obj.getY(),
|
|
name: obj.getName(),
|
|
}))
|
|
).to.eql([
|
|
// The initial instance should be unchanged ("object1").
|
|
{ x: 101, y: 201, name: 'NotSavedSpriteObject' },
|
|
// "object4", which was created dynamically before the save, should be unchanged.
|
|
{ x: 701, y: 801, name: 'NotSavedSpriteObject' },
|
|
// "object7", which was created dynamically after the save, should be unchanged.
|
|
{ x: 1300, y: 1400, name: 'NotSavedSpriteObject' },
|
|
]);
|
|
|
|
// Check variables were restored, and the excluded one was not.
|
|
expect(runtimeScene1.getVariables().get('Variable1').getAsString()).to.be(
|
|
'TestValue'
|
|
); // Restored
|
|
expect(
|
|
runtimeScene1.getVariables().get('Variable2').getAsBoolean()
|
|
).to.be(true); // Restored
|
|
expect(runtimeScene1.getVariables().get('Variable3').getAsNumber()).to.be(
|
|
43
|
|
); // Unchanged
|
|
});
|
|
|
|
it('saves and restores the same running game (keep the scene stack)', async () => {
|
|
// Start a game with multiple scenes.
|
|
const scene1Data = getFakeSceneData({ name: 'Scene1' });
|
|
const scene2Data = getFakeSceneData({ name: 'Scene2' });
|
|
const scene3Data = getFakeSceneData({ name: 'Scene3' });
|
|
|
|
const runtimeGame1 = gdjs.getPixiRuntimeGame({
|
|
layouts: [scene1Data, scene2Data, scene3Data],
|
|
});
|
|
await runtimeGame1._resourcesLoader.loadAllResources(() => {});
|
|
|
|
// Push 3 scenes onto the stack.
|
|
const runtimeScene1 = runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene1',
|
|
});
|
|
const runtimeScene2 = runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene2',
|
|
});
|
|
const runtimeScene3 = runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene3',
|
|
});
|
|
|
|
if (!runtimeScene1 || !runtimeScene2 || !runtimeScene3) {
|
|
throw new Error('Scenes were not created.');
|
|
}
|
|
|
|
// Create objects in each scene at specific positions.
|
|
const object1 = runtimeScene1.createObject('MySpriteObject');
|
|
const object2 = runtimeScene2.createObject('MySpriteObject');
|
|
const object3 = runtimeScene3.createObject('MySpriteObject');
|
|
|
|
if (!object1 || !object2 || !object3) {
|
|
throw new Error('Objects were not created');
|
|
}
|
|
|
|
object1.setX(100);
|
|
object1.setY(200);
|
|
object2.setX(300);
|
|
object2.setY(400);
|
|
object3.setX(500);
|
|
object3.setY(600);
|
|
|
|
// Save the game state.
|
|
const saveState = gdjs.saveState.createGameSaveState(runtimeGame1, {
|
|
profileNames: ['default'],
|
|
});
|
|
|
|
// Do some changes in the game:
|
|
object1.setX(101);
|
|
object1.setY(201);
|
|
object2.setX(301);
|
|
object2.setY(401);
|
|
object3.setX(501);
|
|
object3.setY(601);
|
|
|
|
// Remove the last scene and add a few others (which will be unloaded
|
|
// when we restore the save state).
|
|
runtimeGame1.getSceneStack().pop();
|
|
runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene2',
|
|
});
|
|
runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene2',
|
|
});
|
|
runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene2',
|
|
});
|
|
|
|
// Load the saved state on the same game without clearing the scene stack.
|
|
gdjs.saveState.restoreGameSaveState(runtimeGame1, saveState, {
|
|
profileNames: ['default'],
|
|
clearSceneStack: false,
|
|
});
|
|
|
|
// Verify all 3 scenes have been restored. The first two are unchanged.
|
|
// The last one is a new one.
|
|
const allScenes = runtimeGame1.getSceneStack().getAllScenes();
|
|
expect(allScenes.length).to.be(3);
|
|
expect(allScenes[0].getName()).to.be('Scene1');
|
|
expect(allScenes[0]).to.be(runtimeScene1);
|
|
expect(allScenes[1].getName()).to.be('Scene2');
|
|
expect(allScenes[1]).to.be(runtimeScene2);
|
|
expect(allScenes[2].getName()).to.be('Scene3');
|
|
expect(allScenes[2]).not.to.be(runtimeScene3);
|
|
|
|
// Verify objects are restored at their proper positions.
|
|
const restoredObjects1 = allScenes[0].getObjects('MySpriteObject');
|
|
const restoredObjects2 = allScenes[1].getObjects('MySpriteObject');
|
|
const restoredObjects3 = allScenes[2].getObjects('MySpriteObject');
|
|
|
|
expect(restoredObjects1.length).to.be(1);
|
|
expect(restoredObjects2.length).to.be(1);
|
|
expect(restoredObjects3.length).to.be(1);
|
|
|
|
expect(restoredObjects1[0].getX()).to.be(100);
|
|
expect(restoredObjects1[0].getY()).to.be(200);
|
|
expect(restoredObjects2[0].getX()).to.be(300);
|
|
expect(restoredObjects2[0].getY()).to.be(400);
|
|
expect(restoredObjects3[0].getX()).to.be(500);
|
|
expect(restoredObjects3[0].getY()).to.be(600);
|
|
});
|
|
});
|
|
|
|
describe('Save State restored with specified profile(s)', () => {
|
|
it('saves and restores the same running game (only objects in the specified profiles)', async () => {
|
|
// Start a game with objects configured for different profiles.
|
|
const sceneData = getFakeSceneData({
|
|
name: 'Scene1',
|
|
objects: [
|
|
{
|
|
type: 'Sprite',
|
|
name: 'Profile1Object',
|
|
behaviors: [
|
|
{
|
|
name: 'SaveConfiguration',
|
|
type: 'SaveState::SaveConfiguration',
|
|
defaultProfilePersistence: 'DoNotSave',
|
|
persistedInProfiles: 'profile1',
|
|
},
|
|
],
|
|
effects: [],
|
|
variables: [],
|
|
updateIfNotVisible: false,
|
|
animations: [],
|
|
},
|
|
{
|
|
type: 'Sprite',
|
|
name: 'Profile2Object',
|
|
behaviors: [
|
|
{
|
|
name: 'SaveConfiguration',
|
|
type: 'SaveState::SaveConfiguration',
|
|
defaultProfilePersistence: 'DoNotSave',
|
|
persistedInProfiles: 'profile2',
|
|
},
|
|
],
|
|
effects: [],
|
|
variables: [],
|
|
updateIfNotVisible: false,
|
|
animations: [],
|
|
},
|
|
{
|
|
type: 'Sprite',
|
|
name: 'Profile3Object',
|
|
behaviors: [
|
|
{
|
|
name: 'SaveConfiguration',
|
|
type: 'SaveState::SaveConfiguration',
|
|
defaultProfilePersistence: 'DoNotSave',
|
|
persistedInProfiles: 'profile3',
|
|
},
|
|
],
|
|
effects: [],
|
|
variables: [],
|
|
updateIfNotVisible: false,
|
|
animations: [],
|
|
},
|
|
{
|
|
type: 'Sprite',
|
|
name: 'AllProfilesObject',
|
|
behaviors: [
|
|
{
|
|
name: 'SaveConfiguration',
|
|
type: 'SaveState::SaveConfiguration',
|
|
defaultProfilePersistence: 'DoNotSave',
|
|
persistedInProfiles: 'profile1, profile2, profile3',
|
|
},
|
|
],
|
|
effects: [],
|
|
variables: [],
|
|
updateIfNotVisible: false,
|
|
animations: [],
|
|
},
|
|
],
|
|
instances: [],
|
|
});
|
|
|
|
const runtimeGame1 = gdjs.getPixiRuntimeGame({
|
|
layouts: [sceneData],
|
|
});
|
|
await runtimeGame1._resourcesLoader.loadAllResources(() => {});
|
|
|
|
const runtimeScene1 = runtimeGame1.getSceneStack().push({
|
|
sceneName: 'Scene1',
|
|
});
|
|
if (!runtimeScene1) throw new Error('No current scene was created.');
|
|
|
|
// Create objects for different profiles.
|
|
const profile1Object = runtimeScene1.createObject('Profile1Object');
|
|
const profile2Object = runtimeScene1.createObject('Profile2Object');
|
|
const profile3Object = runtimeScene1.createObject('Profile3Object');
|
|
const bothProfilesObject =
|
|
runtimeScene1.createObject('AllProfilesObject');
|
|
|
|
if (
|
|
!profile1Object ||
|
|
!profile2Object ||
|
|
!profile3Object ||
|
|
!bothProfilesObject
|
|
) {
|
|
throw new Error('Objects were not created');
|
|
}
|
|
|
|
profile1Object.setX(100);
|
|
profile1Object.setY(200);
|
|
profile2Object.setX(300);
|
|
profile2Object.setY(400);
|
|
profile3Object.setX(500);
|
|
profile3Object.setY(600);
|
|
bothProfilesObject.setX(700);
|
|
bothProfilesObject.setY(800);
|
|
|
|
// Save the game state with both profiles.
|
|
const saveState = gdjs.saveState.createGameSaveState(runtimeGame1, {
|
|
// Save with profiles 1 and 2, even if we will restore with only the 'profile1' and 'profile3' profile.
|
|
profileNames: ['profile1', 'profile2'],
|
|
});
|
|
|
|
// Do some changes in the game to verify that the saved state is restored.
|
|
profile1Object.setX(101);
|
|
profile1Object.setY(201);
|
|
profile2Object.setX(301);
|
|
profile2Object.setY(401);
|
|
profile3Object.setX(501);
|
|
profile3Object.setY(601);
|
|
bothProfilesObject.setX(701);
|
|
bothProfilesObject.setY(801);
|
|
|
|
// Load the saved state with only the 'profile1' and 'profile3 profiles.
|
|
gdjs.saveState.restoreGameSaveState(runtimeGame1, saveState, {
|
|
profileNames: ['profile1', 'profile3'],
|
|
clearSceneStack: false,
|
|
});
|
|
|
|
// Verify only profile1 and profile3 objects are restored.
|
|
const restoredProfile1Objects =
|
|
runtimeScene1.getObjects('Profile1Object');
|
|
const restoredProfile2Objects =
|
|
runtimeScene1.getObjects('Profile2Object');
|
|
const restoredProfile3Objects =
|
|
runtimeScene1.getObjects('Profile3Object');
|
|
const restoredAllProfilesObjects =
|
|
runtimeScene1.getObjects('AllProfilesObject');
|
|
|
|
expect(restoredProfile1Objects.length).to.be(1);
|
|
expect(restoredProfile1Objects[0].getX()).to.be(100);
|
|
expect(restoredProfile1Objects[0].getY()).to.be(200);
|
|
|
|
expect(restoredAllProfilesObjects.length).to.be(1);
|
|
expect(restoredAllProfilesObjects[0].getX()).to.be(700);
|
|
expect(restoredAllProfilesObjects[0].getY()).to.be(800);
|
|
|
|
// Profile3 objects should be restored. There are none in the save state, so they
|
|
// are all removed.
|
|
expect(restoredProfile3Objects.length).to.be(0);
|
|
|
|
// Profile2 objects should be left alone.
|
|
expect(restoredProfile2Objects.length).to.be(1);
|
|
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);
|
|
});
|
|
});
|