mirror of
https://github.com/Heretek-AI/GDevelop.git
synced 2026-07-22 01:55:25 -04:00
0fa6ac7423
The cloneObjectWithoutOverwriting was not necessary, and was not updating the existing entries
2768 lines
89 KiB
JavaScript
2768 lines
89 KiB
JavaScript
// @ts-check
|
||
|
||
describe('Multiplayer', () => {
|
||
const delay = (ms) => new Promise((res) => setTimeout(res, ms));
|
||
|
||
/**
|
||
* @param {{name: string}} settings
|
||
* @returns {SceneAndExtensionsData}
|
||
*/
|
||
const getFakeSceneAndExtensionData = ({ name }) => ({
|
||
sceneData: {
|
||
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: [
|
||
{
|
||
type: 'Sprite',
|
||
name: 'MySpriteObject',
|
||
behaviors: [
|
||
{
|
||
name: 'MultiplayerObject',
|
||
type: 'Multiplayer::MultiplayerObjectBehavior',
|
||
playerNumber: 0,
|
||
actionOnPlayerDisconnect: 'Destroy',
|
||
},
|
||
{
|
||
name: 'DummyBehavior',
|
||
type: 'MyDummyExtension::DummyBehavior',
|
||
},
|
||
],
|
||
effects: [],
|
||
// @ts-expect-error ts-migrate(2322) FIXME: Type '{ type: string; name: string; behaviors: nev... Remove this comment to see the full error message
|
||
animations: [],
|
||
variables: [],
|
||
},
|
||
],
|
||
instances: [],
|
||
variables: [],
|
||
usedResources: [],
|
||
},
|
||
usedExtensionsWithVariablesData: [],
|
||
});
|
||
|
||
/**
|
||
*
|
||
* @param {gdjs.RuntimeScene} scene
|
||
* @param {string} objectName
|
||
* @returns {{object: gdjs.RuntimeObject, behavior: gdjs.MultiplayerObjectRuntimeBehavior}[]}
|
||
*/
|
||
const getObjectAndMultiplayerBehaviorsFromScene = (scene, objectName) => {
|
||
const objects = scene.getObjects(objectName);
|
||
if (!objects)
|
||
throw new Error(`No object with name ${objectName} found in scene`);
|
||
return objects.map((object) => {
|
||
/** @type {gdjs.MultiplayerObjectRuntimeBehavior | null} */
|
||
// @ts-ignore - We know this returns a MultiplayerObjectRuntimeBehavior
|
||
const behavior = object.getBehavior('MultiplayerObject');
|
||
if (!behavior)
|
||
throw new Error(
|
||
`No MultiplayerObject behavior found on object ${objectName}`
|
||
);
|
||
return { object, behavior };
|
||
});
|
||
};
|
||
|
||
const makeTestRuntimeSceneWithNetworkId = (timeDelta = 1000 / 60) => {
|
||
const runtimeGame = gdjs.getPixiRuntimeGame();
|
||
gdjs.projectData = {
|
||
// @ts-ignore - we don't set all project props.
|
||
properties: {
|
||
projectUuid: 'fake-hardcoded-project-uuid',
|
||
},
|
||
};
|
||
const runtimeScene = new gdjs.TestRuntimeScene(runtimeGame);
|
||
runtimeScene.loadFromScene(
|
||
getFakeSceneAndExtensionData({ name: 'Scene1' })
|
||
);
|
||
runtimeScene._timeManager.getElapsedTime = function () {
|
||
return timeDelta;
|
||
};
|
||
runtimeScene.networkId = 'fake-hardcoded-scene-network-id';
|
||
return runtimeScene;
|
||
};
|
||
|
||
/**
|
||
* A mocked P2P message data.
|
||
* @implements {gdjs.multiplayerPeerJsHelper.IMessageData}
|
||
*/
|
||
class MockedMessageData {
|
||
/**
|
||
* @param {object} data
|
||
* @param {string} sender
|
||
**/
|
||
constructor(data, sender) {
|
||
this.data = data;
|
||
this.sender = sender;
|
||
}
|
||
|
||
/**
|
||
* The data sent alongside the event.
|
||
*/
|
||
data = {};
|
||
|
||
/**
|
||
* The ID of the sender of the event.
|
||
*/
|
||
sender = '';
|
||
|
||
getData() {
|
||
return this.data;
|
||
}
|
||
|
||
getSender() {
|
||
return this.sender;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* A mocked P2P messages list.
|
||
* @implements {gdjs.multiplayerPeerJsHelper.IMessagesList}
|
||
*/
|
||
class MockedMessagesList {
|
||
data = [];
|
||
messageName = 'some-message-name';
|
||
|
||
constructor(messageName) {
|
||
this.messageName = messageName;
|
||
}
|
||
|
||
getName() {
|
||
return this.messageName;
|
||
}
|
||
|
||
/**
|
||
* @param {object} newData
|
||
* @param {string} sender
|
||
*/
|
||
pushMessage(newData, sender) {
|
||
this.data.push(new MockedMessageData(newData, sender));
|
||
}
|
||
|
||
getMessages() {
|
||
return this.data;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Create a mocked P2P handler.
|
||
* It stores the messages sent to/from peers.
|
||
*/
|
||
const createMultiplayerManagersMock = () => {
|
||
/**
|
||
* @type {{ currentPeerId: string, otherPeerIds: string[], justDisconnectedPeers: string[] }}
|
||
*/
|
||
const p2pState = {
|
||
currentPeerId: '',
|
||
otherPeerIds: [],
|
||
justDisconnectedPeers: [],
|
||
};
|
||
|
||
/** @type {Record<string, Map<string, MockedMessagesList>>} */
|
||
const peerAllMessagesMap = {};
|
||
|
||
/** @type {Record<string, gdjs.MultiplayerMessageManager>} */
|
||
const peerMultiplayerMessageManager = {};
|
||
|
||
/** @type {Record<string, gdjs.MultiplayerVariablesManager>} */
|
||
const peerMultiplayerVariablesManager = {};
|
||
|
||
const getPeerMessages = (peerId) =>
|
||
(peerAllMessagesMap[peerId] = peerAllMessagesMap[peerId] || new Map());
|
||
|
||
/**
|
||
* @param {string} messageName
|
||
* @returns {gdjs.multiplayerPeerJsHelper.IMessagesList}
|
||
*/
|
||
const getOrCreateMessagesList = (messageName) => {
|
||
const allMessagesMap = getPeerMessages(p2pState.currentPeerId);
|
||
const messagesList = allMessagesMap.get(messageName);
|
||
if (messagesList) return messagesList;
|
||
const newMessagesList = new MockedMessagesList(messageName);
|
||
allMessagesMap.set(messageName, newMessagesList);
|
||
return newMessagesList;
|
||
};
|
||
|
||
/**
|
||
* @param {string[]} peerIds
|
||
* @param {string} messageName
|
||
* @param {object} messageData
|
||
*/
|
||
const sendDataTo = async (peerIds, messageName, messageData) => {
|
||
for (const peerId of peerIds) {
|
||
// console.log(`## SENDING DATA TO ${peerId}:`, messageName, messageData);
|
||
const peerAllMessagesMap = getPeerMessages(peerId);
|
||
let peerMessagesList = peerAllMessagesMap.get(messageName);
|
||
if (!peerMessagesList) {
|
||
peerMessagesList = new MockedMessagesList(messageName);
|
||
peerAllMessagesMap.set(messageName, peerMessagesList);
|
||
}
|
||
|
||
// We clone the message data to simulate the fact that it's sent over the network,
|
||
// so that the original message data is not modified by the receiver.
|
||
const clonedMessageData = JSON.parse(JSON.stringify(messageData));
|
||
|
||
peerMessagesList.pushMessage(clonedMessageData, p2pState.currentPeerId);
|
||
}
|
||
};
|
||
|
||
/** @type {typeof gdjs.multiplayerPeerJsHelper} */
|
||
const peerJsHelperMock = {
|
||
// @ts-ignore - this is a mock so private properties can't be the same.
|
||
MessagesList: MockedMessagesList,
|
||
MessageData: MockedMessageData,
|
||
getOrCreateMessagesList,
|
||
connect: (id) => {},
|
||
disconnectFromAllPeers: () => {},
|
||
sendDataTo,
|
||
getAllMessagesMap: () => getPeerMessages(p2pState.currentPeerId),
|
||
useCustomBrokerServer: () => {},
|
||
useDefaultBrokerServer: () => {},
|
||
useCustomICECandidate: () => {},
|
||
forceUseRelayServer: (shouldUseRelayServer) => {},
|
||
getCurrentId: () => p2pState.currentPeerId,
|
||
isReady: () => true,
|
||
getJustDisconnectedPeers: () => p2pState.justDisconnectedPeers,
|
||
getAllPeers: () => p2pState.otherPeerIds,
|
||
};
|
||
|
||
gdjs.multiplayerPeerJsHelper = peerJsHelperMock;
|
||
|
||
/**
|
||
* Helper to clear all messages stored in the peer messages lists.
|
||
*/
|
||
const markAllPeerMessagesAsProcessed = () => {
|
||
for (const allMessagesList of Object.values(peerAllMessagesMap)) {
|
||
for (const messagesList of allMessagesList.values()) {
|
||
messagesList.data = [];
|
||
}
|
||
}
|
||
};
|
||
|
||
const expectNoMessagesToBeProcessed = () => {
|
||
for (const allMessagesList of Object.values(peerAllMessagesMap)) {
|
||
for (const messagesList of allMessagesList.values()) {
|
||
expect(messagesList.getMessages().length).to.be(0);
|
||
}
|
||
}
|
||
};
|
||
|
||
/**
|
||
* @param {{ playerNumber: number, allConnectedPlayers: {playerNumber: number, peerId: string}[], justDisconnectedPeers?: string[]}} options
|
||
*/
|
||
const switchToPeer = ({
|
||
playerNumber,
|
||
allConnectedPlayers,
|
||
justDisconnectedPeers,
|
||
}) => {
|
||
const connectedPlayer = allConnectedPlayers.find(
|
||
(player) => player.playerNumber === playerNumber
|
||
);
|
||
if (!connectedPlayer)
|
||
throw new Error(`No player found for player ${playerNumber}`);
|
||
const peerId = connectedPlayer.peerId;
|
||
// console.log('## SWITCHING TO PEER', peerId);
|
||
|
||
// Switch the state of the peerJs mock.
|
||
p2pState.currentPeerId = peerId;
|
||
p2pState.justDisconnectedPeers = justDisconnectedPeers || [];
|
||
// Player 1 is connected to everyone else, and everyone else is connected to player 1.
|
||
if (playerNumber === 1) {
|
||
p2pState.otherPeerIds = allConnectedPlayers
|
||
.filter((player) => player.playerNumber !== 1)
|
||
.map((player) => player.peerId);
|
||
} else {
|
||
p2pState.otherPeerIds = allConnectedPlayers
|
||
.filter((player) => player.playerNumber === 1)
|
||
.map((player) => player.peerId);
|
||
}
|
||
|
||
// Switch the state of the MultiplayerMessageManager.
|
||
gdjs.multiplayerMessageManager = peerMultiplayerMessageManager[peerId] =
|
||
peerMultiplayerMessageManager[peerId] ||
|
||
gdjs.makeMultiplayerMessageManager();
|
||
|
||
// Switch the state of the MultiplayerVariablesManager.
|
||
gdjs.multiplayerVariablesManager = peerMultiplayerVariablesManager[
|
||
peerId
|
||
] =
|
||
peerMultiplayerVariablesManager[peerId] ||
|
||
gdjs.makeMultiplayerVariablesManager();
|
||
// Switch the state of the game.
|
||
gdjs.multiplayer.playerNumber = playerNumber;
|
||
};
|
||
|
||
/**
|
||
* Helper to fast forward a bit of time in players games, so that heartbeats
|
||
* are sent and all players are aware of each other.
|
||
* @param {{ playerNumber: number, peerId: string, isHost?: boolean }[]} players
|
||
*/
|
||
const initiateGameWithPlayers = (players) => {
|
||
// Find the host.
|
||
const host = players.find((player) => player.isHost);
|
||
if (!host)
|
||
throw new Error('No host defined in players, cannot initiate game.');
|
||
|
||
// Create the instances of the MultiplayerMessageManager and MultiplayerVariablesManager
|
||
// for each player.
|
||
for (const player of players) {
|
||
peerMultiplayerMessageManager[player.peerId] =
|
||
gdjs.makeMultiplayerMessageManager();
|
||
peerMultiplayerVariablesManager[player.peerId] =
|
||
gdjs.makeMultiplayerVariablesManager();
|
||
|
||
// Define the host for everyone.
|
||
gdjs.multiplayer.hostPeerId = host.peerId;
|
||
}
|
||
|
||
// Use a scene to simulate the game loop moving forward.
|
||
const runtimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Speed up time on player 1 to send heartbeats.
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers: players,
|
||
});
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Speed up time on other players to receive heartbeats and send them back.
|
||
for (const player of players) {
|
||
if (player.playerNumber === 1) continue;
|
||
switchToPeer({
|
||
playerNumber: player.playerNumber,
|
||
allConnectedPlayers: players,
|
||
});
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Speed up time on player 1 to receive heartbeats and send them back.
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers: players,
|
||
});
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Speed up time on other players to receive updated heartbeats with players info.
|
||
for (const player of players) {
|
||
if (player.playerNumber === 1) continue;
|
||
switchToPeer({
|
||
playerNumber: player.playerNumber,
|
||
allConnectedPlayers: players,
|
||
});
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
};
|
||
|
||
const logMessages = () => {
|
||
Object.keys(peerAllMessagesMap).forEach((peerId) => {
|
||
console.log(`## PEER ${peerId} messages:`);
|
||
for (const [messageName, messagesList] of peerAllMessagesMap[peerId]) {
|
||
console.log(
|
||
`${messageName}: ${JSON.stringify(messagesList.getMessages())}`
|
||
);
|
||
}
|
||
});
|
||
};
|
||
|
||
return {
|
||
initiateGameWithPlayers,
|
||
switchToPeer,
|
||
logMessages,
|
||
markAllPeerMessagesAsProcessed,
|
||
expectNoMessagesToBeProcessed,
|
||
};
|
||
};
|
||
|
||
let _originalP2pIfAny = undefined;
|
||
|
||
beforeEach(() => {
|
||
_originalP2pIfAny = gdjs.multiplayerPeerJsHelper;
|
||
gdjs.multiplayer.disableMultiplayerForTesting = false;
|
||
gdjs.multiplayer._isLobbyGameRunning = true;
|
||
gdjs.multiplayer._isReadyToSendOrReceiveGameUpdateMessages = true;
|
||
// Sync as fast as possible for tests.
|
||
gdjs.multiplayer._objectMaxSyncRate = Infinity;
|
||
});
|
||
afterEach(() => {
|
||
gdjs.multiplayerPeerJsHelper = _originalP2pIfAny;
|
||
gdjs.multiplayer.disableMultiplayerForTesting = true;
|
||
gdjs.multiplayer._isLobbyGameRunning = false;
|
||
gdjs.multiplayer._isReadyToSendOrReceiveGameUpdateMessages = false;
|
||
gdjs.multiplayer._objectMaxSyncRate =
|
||
gdjs.multiplayer.DEFAULT_OBJECT_MAX_SYNC_RATE;
|
||
});
|
||
|
||
describe('Single scene tests', () => {
|
||
it('synchronizes scene/global variables from the host to other players', () => {
|
||
const {
|
||
switchToPeer,
|
||
markAllPeerMessagesAsProcessed,
|
||
initiateGameWithPlayers,
|
||
} = createMultiplayerManagersMock();
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p1StringVariable = new gdjs.Variable();
|
||
p1StringVariable.setString('Hello from remote world');
|
||
const p1NumberVariable = new gdjs.Variable();
|
||
p1NumberVariable.setNumber(42);
|
||
const p1BooleanVariable = new gdjs.Variable();
|
||
p1BooleanVariable.setBoolean(false);
|
||
|
||
p1RuntimeScene.getVariables().add('MyString_Variable', p1StringVariable);
|
||
p1RuntimeScene.getVariables().add('MyNumber_Variable', p1NumberVariable);
|
||
p1RuntimeScene
|
||
.getVariables()
|
||
.add('MyBoolean_Variable', p1BooleanVariable);
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
markAllPeerMessagesAsProcessed();
|
||
expect(p2RuntimeScene.getVariables().has('MyString_Variable')).to.be(
|
||
true
|
||
);
|
||
expect(p2RuntimeScene.getVariables().has('MyNumber_Variable')).to.be(
|
||
true
|
||
);
|
||
expect(p2RuntimeScene.getVariables().has('MyBoolean_Variable')).to.be(
|
||
true
|
||
);
|
||
expect(
|
||
p2RuntimeScene.getVariables().get('MyString_Variable').getAsString()
|
||
).to.be('Hello from remote world');
|
||
expect(
|
||
p2RuntimeScene.getVariables().get('MyNumber_Variable').getAsNumber()
|
||
).to.be(42);
|
||
expect(
|
||
p2RuntimeScene.getVariables().get('MyBoolean_Variable').getAsBoolean()
|
||
).to.be(false);
|
||
|
||
// Also check global variables.
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1GlobalStringVariable = new gdjs.Variable();
|
||
p1GlobalStringVariable.setString('Hello from remote global world');
|
||
const p1GlobalNumberVariable = new gdjs.Variable();
|
||
p1GlobalNumberVariable.setNumber(142);
|
||
const p1GlobalBooleanVariable = new gdjs.Variable();
|
||
p1GlobalBooleanVariable.setBoolean(false);
|
||
const p1GlobalArrayVariable = new gdjs.Variable();
|
||
p1GlobalArrayVariable.pushValue('Hello from remote global array');
|
||
p1GlobalArrayVariable.pushValue(143);
|
||
p1GlobalArrayVariable.pushValue(true);
|
||
const p1GlobalStructureVariable = new gdjs.Variable();
|
||
const p1GlobalStructureVariableChildString = new gdjs.Variable();
|
||
p1GlobalStructureVariableChildString.setString(
|
||
'Hello from remote global structure'
|
||
);
|
||
const p1GlobalStructureVariableChildNumber = new gdjs.Variable();
|
||
p1GlobalStructureVariableChildNumber.setNumber(144);
|
||
const p1GlobalStructureVariableChildBoolean = new gdjs.Variable();
|
||
p1GlobalStructureVariableChildBoolean.setBoolean(true);
|
||
p1GlobalStructureVariable.addChild(
|
||
'first',
|
||
p1GlobalStructureVariableChildString
|
||
);
|
||
p1GlobalStructureVariable.addChild(
|
||
'second',
|
||
p1GlobalStructureVariableChildNumber
|
||
);
|
||
p1GlobalStructureVariable.addChild(
|
||
'third',
|
||
p1GlobalStructureVariableChildBoolean
|
||
);
|
||
|
||
p1RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.add('MyGlobalStringVariable', p1GlobalStringVariable);
|
||
p1RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.add('MyGlobalNumberVariable', p1GlobalNumberVariable);
|
||
p1RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.add('MyGlobalBooleanVariable', p1GlobalBooleanVariable);
|
||
p1RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.add('MyGlobalArrayVariable', p1GlobalArrayVariable);
|
||
p1RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.add('MyGlobalStructureVariable', p1GlobalStructureVariable);
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
markAllPeerMessagesAsProcessed();
|
||
expect(
|
||
p2RuntimeScene.getGame().getVariables().has('MyGlobalStringVariable')
|
||
).to.be(true);
|
||
expect(
|
||
p2RuntimeScene.getGame().getVariables().has('MyGlobalNumberVariable')
|
||
).to.be(true);
|
||
expect(
|
||
p2RuntimeScene.getGame().getVariables().has('MyGlobalBooleanVariable')
|
||
).to.be(true);
|
||
expect(
|
||
p2RuntimeScene.getGame().getVariables().has('MyGlobalArrayVariable')
|
||
).to.be(true);
|
||
expect(
|
||
p2RuntimeScene.getGame().getVariables().has('MyGlobalStructureVariable')
|
||
).to.be(true);
|
||
|
||
expect(
|
||
p2RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalStringVariable')
|
||
.getAsString()
|
||
).to.be('Hello from remote global world');
|
||
expect(
|
||
p2RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalNumberVariable')
|
||
.getAsNumber()
|
||
).to.be(142);
|
||
expect(
|
||
p2RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalBooleanVariable')
|
||
.getAsBoolean()
|
||
).to.be(false);
|
||
const p2GlobalArrayVariable = p2RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalArrayVariable')
|
||
.getAllChildrenArray();
|
||
expect(p2GlobalArrayVariable.length).to.be(3);
|
||
expect(p2GlobalArrayVariable[0].getAsString()).to.be(
|
||
'Hello from remote global array'
|
||
);
|
||
expect(p2GlobalArrayVariable[1].getAsNumber()).to.be(143);
|
||
expect(p2GlobalArrayVariable[2].getAsBoolean()).to.be(true);
|
||
const p2GlobalStructureVariable = p2RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalStructureVariable');
|
||
expect(p2GlobalStructureVariable.hasChild('first')).to.be(true);
|
||
expect(p2GlobalStructureVariable.hasChild('second')).to.be(true);
|
||
expect(p2GlobalStructureVariable.hasChild('third')).to.be(true);
|
||
expect(p2GlobalStructureVariable.getChild('first').getAsString()).to.be(
|
||
'Hello from remote global structure'
|
||
);
|
||
expect(p2GlobalStructureVariable.getChild('second').getAsNumber()).to.be(
|
||
144
|
||
);
|
||
expect(p2GlobalStructureVariable.getChild('third').getAsBoolean()).to.be(
|
||
true
|
||
);
|
||
});
|
||
|
||
it('overrides a scene/global variable, modified by a player, when synchronized by the host', () => {
|
||
const {
|
||
switchToPeer,
|
||
markAllPeerMessagesAsProcessed,
|
||
expectNoMessagesToBeProcessed,
|
||
initiateGameWithPlayers,
|
||
} = createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p1Variable = new gdjs.Variable();
|
||
p1Variable.setString('Hello from remote world');
|
||
p1RuntimeScene.getVariables().add('MyVariable', p1Variable);
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
// Verify player 2 can create variables, but the one from the host will override any value set for it
|
||
// by player 2.
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
{
|
||
const variable = new gdjs.Variable();
|
||
variable.setString('This will be overriden');
|
||
p2RuntimeScene.getVariables().add('MyVariable', variable);
|
||
}
|
||
{
|
||
const variable = new gdjs.Variable();
|
||
variable.setString('Something else');
|
||
p2RuntimeScene.getVariables().add('MyOtherVariable', variable);
|
||
}
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
markAllPeerMessagesAsProcessed();
|
||
expect(
|
||
p2RuntimeScene.getVariables().get('MyVariable').getAsString()
|
||
).to.be('Hello from remote world');
|
||
expect(
|
||
p2RuntimeScene.getVariables().get('MyOtherVariable').getAsString()
|
||
).to.be('Something else');
|
||
|
||
expectNoMessagesToBeProcessed();
|
||
|
||
// Check the host sends again the variable, even if not changed, for reliability
|
||
// (allows to work around a dropped message, without using a real acknowledgement).
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Check the variable on player 2 is overriden again.
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p2RuntimeScene
|
||
.getVariables()
|
||
.get('MyVariable')
|
||
.setString('Changed value that will be overriden again');
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
expect(
|
||
p2RuntimeScene.getVariables().get('MyVariable').getAsString()
|
||
).to.be('Hello from remote world');
|
||
expect(
|
||
p2RuntimeScene.getVariables().get('MyOtherVariable').getAsString()
|
||
).to.be('Something else');
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
expectNoMessagesToBeProcessed();
|
||
});
|
||
|
||
it('synchronizes a scene/global variable from a player to the host to other players', () => {
|
||
const {
|
||
switchToPeer,
|
||
markAllPeerMessagesAsProcessed,
|
||
expectNoMessagesToBeProcessed,
|
||
initiateGameWithPlayers,
|
||
} = createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
{ playerNumber: 3, peerId: 'player-3' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p1SceneVariable = new gdjs.Variable();
|
||
p1SceneVariable.setString('This will be overriden');
|
||
p1RuntimeScene.getVariables().add('MyVariable', p1SceneVariable);
|
||
p1SceneVariable.setPlayerOwnership(3); // Ownership is given to player 3.
|
||
const p1GlobalVariable = new gdjs.Variable();
|
||
p1GlobalVariable.setString('This will be overriden');
|
||
p1RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.add('MyGlobalVariable', p1GlobalVariable);
|
||
p1GlobalVariable.setPlayerOwnership(3); // Ownership is given to player 3.
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
// Create the variable on player 2 too.
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p2Scenevariable = new gdjs.Variable();
|
||
p2Scenevariable.setString('This will be overriden');
|
||
p2RuntimeScene.getVariables().add('MyVariable', p2Scenevariable);
|
||
p2Scenevariable.setPlayerOwnership(3); // Ownership is given to player 3.
|
||
const p2GlobalVariable = new gdjs.Variable();
|
||
p2GlobalVariable.setString('This will be overriden');
|
||
p2RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.add('MyGlobalVariable', p2GlobalVariable);
|
||
p2GlobalVariable.setPlayerOwnership(3); // Ownership is given to player 3.
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
// Create the variable on player 3 too, which is the owner.
|
||
const p3RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p3SceneVariable = new gdjs.Variable();
|
||
p3SceneVariable.setString('This will be overriden');
|
||
p3RuntimeScene.getVariables().add('MyVariable', p3SceneVariable);
|
||
p3SceneVariable.setPlayerOwnership(3); // Ownership is given to player 3.
|
||
const p3GlobalVariable = new gdjs.Variable();
|
||
p3GlobalVariable.setString('This will be overriden');
|
||
p3RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.add('MyGlobalVariable', p3GlobalVariable);
|
||
p3GlobalVariable.setPlayerOwnership(3); // Ownership is given to player 3.
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
expectNoMessagesToBeProcessed();
|
||
|
||
// Change the variables on player 3.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p3RuntimeScene
|
||
.getVariables()
|
||
.get('MyVariable')
|
||
.setString('Changed by player 3');
|
||
p3RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalVariable')
|
||
.setString('Changed by player 3');
|
||
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
// Check the variables are synchronized on the host.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
expect(
|
||
p1RuntimeScene.getVariables().get('MyVariable').getAsString()
|
||
).to.be('Changed by player 3');
|
||
expect(
|
||
p1RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalVariable')
|
||
.getAsString()
|
||
).to.be('Changed by player 3');
|
||
}
|
||
|
||
// Check the variable is synchronized on the other player.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
expect(
|
||
p2RuntimeScene.getVariables().get('MyVariable').getAsString()
|
||
).to.be('Changed by player 3');
|
||
expect(
|
||
p2RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalVariable')
|
||
.getAsString()
|
||
).to.be('Changed by player 3');
|
||
}
|
||
});
|
||
|
||
it('does not synchronize a scene/global variable from players if defined as not synchronized', () => {
|
||
const {
|
||
switchToPeer,
|
||
markAllPeerMessagesAsProcessed,
|
||
expectNoMessagesToBeProcessed,
|
||
initiateGameWithPlayers,
|
||
} = createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
{ playerNumber: 3, peerId: 'player-3' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p1SceneVariable = new gdjs.Variable();
|
||
p1SceneVariable.setString('P1 is master of this variable');
|
||
p1RuntimeScene.getVariables().add('MyVariable', p1SceneVariable);
|
||
p1SceneVariable.disableSynchronization(); // Disable synchronization.
|
||
const p1GlobalVariable = new gdjs.Variable();
|
||
p1GlobalVariable.setString('P1 is master of this variable');
|
||
p1RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.add('MyGlobalVariable', p1GlobalVariable);
|
||
p1GlobalVariable.disableSynchronization(); // Disable synchronization.
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
// Create the variable on player 2 too.
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p2Scenevariable = new gdjs.Variable();
|
||
p2Scenevariable.setString('P2 is master of this variable');
|
||
p2RuntimeScene.getVariables().add('MyVariable', p2Scenevariable);
|
||
p2Scenevariable.disableSynchronization(); // Disable synchronization.
|
||
const p2GlobalVariable = new gdjs.Variable();
|
||
p2GlobalVariable.setString('This will be overriden');
|
||
p2RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.add('MyGlobalVariable', p2GlobalVariable);
|
||
p2GlobalVariable.disableSynchronization(); // Disable synchronization.
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
// Create the variable on player 3 too.
|
||
const p3RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p3SceneVariable = new gdjs.Variable();
|
||
p3SceneVariable.setString('P3 is master of this variable');
|
||
p3RuntimeScene.getVariables().add('MyVariable', p3SceneVariable);
|
||
p3SceneVariable.disableSynchronization(); // Disable synchronization.
|
||
const p3GlobalVariable = new gdjs.Variable();
|
||
p3GlobalVariable.setString('P3 is master of this variable');
|
||
p3RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.add('MyGlobalVariable', p3GlobalVariable);
|
||
p3GlobalVariable.disableSynchronization(); // Disable synchronization.
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
expectNoMessagesToBeProcessed();
|
||
|
||
// Change the variables on player 3.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p3RuntimeScene
|
||
.getVariables()
|
||
.get('MyVariable')
|
||
.setString('Changed by player 3');
|
||
p3RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalVariable')
|
||
.setString('Changed by player 3');
|
||
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
// Change the variables on player 2.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p2RuntimeScene
|
||
.getVariables()
|
||
.get('MyVariable')
|
||
.setString('Changed by player 2');
|
||
p2RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalVariable')
|
||
.setString('Changed by player 2');
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Change the variables on player 1.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p1RuntimeScene
|
||
.getVariables()
|
||
.get('MyVariable')
|
||
.setString('Changed by player 1');
|
||
p1RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalVariable')
|
||
.setString('Changed by player 1');
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Check the variables have not been overriden on player 2.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
expect(
|
||
p2RuntimeScene.getVariables().get('MyVariable').getAsString()
|
||
).to.be('Changed by player 2');
|
||
expect(
|
||
p2RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalVariable')
|
||
.getAsString()
|
||
).to.be('Changed by player 2');
|
||
}
|
||
|
||
// Check the variables have not been overriden on player 3.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
expect(
|
||
p3RuntimeScene.getVariables().get('MyVariable').getAsString()
|
||
).to.be('Changed by player 3');
|
||
expect(
|
||
p3RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalVariable')
|
||
.getAsString()
|
||
).to.be('Changed by player 3');
|
||
}
|
||
|
||
// Check the variables have not been overriden on the host.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
expect(
|
||
p1RuntimeScene.getVariables().get('MyVariable').getAsString()
|
||
).to.be('Changed by player 1');
|
||
expect(
|
||
p1RuntimeScene
|
||
.getGame()
|
||
.getVariables()
|
||
.get('MyGlobalVariable')
|
||
.getAsString()
|
||
).to.be('Changed by player 1');
|
||
}
|
||
});
|
||
|
||
it('synchronizes objects from the host to other players', () => {
|
||
const {
|
||
switchToPeer,
|
||
markAllPeerMessagesAsProcessed,
|
||
initiateGameWithPlayers,
|
||
} = createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
// Create an instance on the host's game:
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p1RuntimeScene.createObject('MySpriteObject');
|
||
const { object: p1SpriteObjectOriginal } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
p1SpriteObjectOriginal.setX(142);
|
||
p1SpriteObjectOriginal.setY(143);
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Check the object is created on the other peer.
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p2Objects = p2RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p2Objects) throw new Error('No objects found');
|
||
expect(p2Objects.length).to.be(0);
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
markAllPeerMessagesAsProcessed();
|
||
|
||
const { object: p2SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
expect(p2SpriteObject.getX()).to.be(142);
|
||
expect(p2SpriteObject.getY()).to.be(143);
|
||
|
||
// Move the object on the host's game:
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { object: p1SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
p1SpriteObject.setX(242);
|
||
p1SpriteObject.setY(243);
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Check the object is moved on the other peer.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
markAllPeerMessagesAsProcessed();
|
||
|
||
const { object: p2SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p2SpriteObject.getX()).to.be(242);
|
||
expect(p2SpriteObject.getY()).to.be(243);
|
||
}
|
||
|
||
// Destroy the object on the host's game:
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { object: p1SpriteObject1 } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
p1SpriteObject1.deleteFromScene();
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Check the object is deleted on the other peer.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
markAllPeerMessagesAsProcessed();
|
||
|
||
const p2Objects = p2RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p2Objects) throw new Error('No objects found');
|
||
|
||
expect(p2Objects.length).to.be(0);
|
||
}
|
||
});
|
||
|
||
it('synchronizes objects from a player to the host to other players', () => {
|
||
const {
|
||
switchToPeer,
|
||
markAllPeerMessagesAsProcessed,
|
||
initiateGameWithPlayers,
|
||
} = createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
{ playerNumber: 3, peerId: 'player-3' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
// Create an instance on a player:
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p2RuntimeScene.createObject('MySpriteObject');
|
||
const {
|
||
object: mySpriteObjectOriginal,
|
||
behavior: mySpriteObjectBehavior,
|
||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
mySpriteObjectOriginal.setX(142);
|
||
mySpriteObjectOriginal.setY(143);
|
||
mySpriteObjectBehavior.setPlayerObjectOwnership(2);
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Check the object is created on the host's game.
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p1Objects = p1RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p1Objects) throw new Error('No objects found');
|
||
expect(p1Objects.length).to.be(0);
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { object: p1SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p1SpriteObject.getX()).to.be(142);
|
||
expect(p1SpriteObject.getY()).to.be(143);
|
||
|
||
// Check the object is created on the other player.
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p3RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p3Objects = p3RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p3Objects) throw new Error('No objects found');
|
||
expect(p3Objects.length).to.be(0);
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { object: p3SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p3SpriteObject.getX()).to.be(142);
|
||
expect(p3SpriteObject.getY()).to.be(143);
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
|
||
// Move the object on the player:
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { object: p2SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
p2SpriteObject.setX(242);
|
||
p2SpriteObject.setY(243);
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Check the object is moved on the host's game.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { object: p1SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p1SpriteObject.getX()).to.be(242);
|
||
expect(p1SpriteObject.getY()).to.be(243);
|
||
}
|
||
|
||
// Check the object is moved on the other player.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
markAllPeerMessagesAsProcessed();
|
||
|
||
const { object: p3SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p3SpriteObject.getX()).to.be(242);
|
||
expect(p3SpriteObject.getY()).to.be(243);
|
||
}
|
||
|
||
// Destroy the object (on player 2):
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { object: p2SpriteObject1 } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
p2SpriteObject1.deleteFromScene();
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Check the object is deleted on the host's game.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1ObjectsAndBehaviors = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
);
|
||
expect(p1ObjectsAndBehaviors.length).to.be(1);
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
const p1ObjectsAndBehaviorsUpdated =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
);
|
||
expect(p1ObjectsAndBehaviorsUpdated.length).to.be(0);
|
||
}
|
||
|
||
// Check the object is deleted on the other player.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p3ObjectsAndBehaviors = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
);
|
||
expect(p3ObjectsAndBehaviors.length).to.be(1);
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
const p3ObjectsAndBehaviorsUpdated =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
);
|
||
expect(p3ObjectsAndBehaviorsUpdated.length).to.be(0);
|
||
}
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
});
|
||
|
||
it('allows ownership to change from host to a player to another player', () => {
|
||
const {
|
||
switchToPeer,
|
||
markAllPeerMessagesAsProcessed,
|
||
expectNoMessagesToBeProcessed,
|
||
initiateGameWithPlayers,
|
||
} = createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
{ playerNumber: 3, peerId: 'player-3' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
// Create an instance on the host's game:
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p1RuntimeScene.createObject('MySpriteObject');
|
||
const { object: p1SpriteObjectOriginal } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
p1SpriteObjectOriginal.setX(142);
|
||
p1SpriteObjectOriginal.setY(143);
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Check the object is created on the players.
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
const { object: p2SpriteObjectOriginal } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
expect(p2SpriteObjectOriginal.getX()).to.be(142);
|
||
expect(p2SpriteObjectOriginal.getY()).to.be(143);
|
||
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p3RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
const { object: p3SpriteObjectOriginal } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
expect(p3SpriteObjectOriginal.getX()).to.be(142);
|
||
expect(p3SpriteObjectOriginal.getY()).to.be(143);
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
expectNoMessagesToBeProcessed();
|
||
|
||
// Check player 3 can get ownership (and can directly move the instance, without waiting for the
|
||
// host to acknowledge the change).
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const {
|
||
object: p3SpriteObject,
|
||
behavior: p3MultiplayerObjectBehavior,
|
||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
p3SpriteObject.setX(342);
|
||
p3SpriteObject.setY(343);
|
||
p3MultiplayerObjectBehavior.setPlayerObjectOwnership(3);
|
||
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Check the host is notified of the new ownership (and the new position).
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const {
|
||
object: p1SpriteObject,
|
||
behavior: p1MultiplayerObjectBehavior,
|
||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p1MultiplayerObjectBehavior.getPlayerObjectOwnership()).to.be(3);
|
||
expect(p1SpriteObject.getX()).to.be(342);
|
||
expect(p1SpriteObject.getY()).to.be(343);
|
||
}
|
||
|
||
// Check the player 2 is notified of the new ownership (and the new position).
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const {
|
||
object: p2SpriteObject,
|
||
behavior: p2MultiplayerObjectBehavior,
|
||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p2MultiplayerObjectBehavior.getPlayerObjectOwnership()).to.be(3);
|
||
expect(p2SpriteObject.getX()).to.be(342);
|
||
expect(p2SpriteObject.getY()).to.be(343);
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
markAllPeerMessagesAsProcessed();
|
||
expectNoMessagesToBeProcessed();
|
||
}
|
||
|
||
// Check player 2 can get ownership.
|
||
// It will also communicate the new position/changes to the instance.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const {
|
||
object: p2SpriteObject,
|
||
behavior: p2MultiplayerObjectBehavior,
|
||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
p2MultiplayerObjectBehavior.setPlayerObjectOwnership(2);
|
||
p2SpriteObject.setX(242);
|
||
p2SpriteObject.setY(243);
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Check the host is notified of the new ownership.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { behavior: p1MultiplayerObjectBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p1MultiplayerObjectBehavior.getPlayerObjectOwnership()).to.be(3);
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { behavior: p1MultiplayerObjectBehaviorUpdated } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
expect(
|
||
p1MultiplayerObjectBehaviorUpdated.getPlayerObjectOwnership()
|
||
).to.be(2);
|
||
}
|
||
|
||
// Check the player 3 is notified of the new ownership.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { behavior: p3MultiplayerObjectBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p3MultiplayerObjectBehavior.getPlayerObjectOwnership()).to.be(3);
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { behavior: p3MultiplayerObjectBehaviorUpdated } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
expect(
|
||
p3MultiplayerObjectBehaviorUpdated.getPlayerObjectOwnership()
|
||
).to.be(2);
|
||
}
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
|
||
// Check that the position given by player 2 is updated on the host and player 3.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { behavior: p1MultiplayerObjectBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p1MultiplayerObjectBehavior.getPlayerObjectOwnership()).to.be(2);
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
const { object: p1SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
expect(p1SpriteObject.getX()).to.be(242);
|
||
expect(p1SpriteObject.getY()).to.be(243);
|
||
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { behavior: p3MultiplayerObjectBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p3MultiplayerObjectBehavior.getPlayerObjectOwnership()).to.be(2);
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
const { object: p3SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
expect(p3SpriteObject.getX()).to.be(242);
|
||
expect(p3SpriteObject.getY()).to.be(243);
|
||
}
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
expectNoMessagesToBeProcessed();
|
||
});
|
||
|
||
it('reconciles an instance owned by a player with a "ghost" instance created on other peers without a network ID (as not owned by them)', () => {
|
||
const {
|
||
switchToPeer,
|
||
markAllPeerMessagesAsProcessed,
|
||
initiateGameWithPlayers,
|
||
} = createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
{ playerNumber: 3, peerId: 'player-3' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
// Create an instance on a player:
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p2RuntimeScene.createObject('MySpriteObject');
|
||
const { object: p2SpriteObject, behavior: p2SpriteObjectBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
p2SpriteObject.setX(142);
|
||
p2SpriteObject.setY(143);
|
||
p2SpriteObjectBehavior.setPlayerObjectOwnership(2);
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Check the object is created on the host's game.
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p1Objects = p1RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p1Objects) throw new Error('No objects found');
|
||
expect(p1Objects.length).to.be(0);
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { object: p1SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p1SpriteObject.getX()).to.be(142);
|
||
expect(p1SpriteObject.getY()).to.be(143);
|
||
|
||
// Check the object is created on the other player.
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p3RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p3Objects = p3RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p3Objects) throw new Error('No objects found');
|
||
expect(p3Objects.length).to.be(0);
|
||
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { object: p3SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p3SpriteObject.getX()).to.be(142);
|
||
expect(p3SpriteObject.getY()).to.be(143);
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
|
||
// Now, create a new instance on the host and player 3, but owned by player 2.
|
||
// We call this in this test a "ghost" instance as it would be deleted if not "reconcilied".
|
||
// We can assume it's because there is some common logic running for all players
|
||
// resulting in the creation of this instance everywhere.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
p1RuntimeScene.createObject('MySpriteObject');
|
||
const { behavior: p1MultiplayerBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[1]; // The new instance
|
||
p1MultiplayerBehavior.setPlayerObjectOwnership(2);
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const p1ObjectsAndBehaviors = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
);
|
||
expect(p1ObjectsAndBehaviors.length).to.be(2);
|
||
const { object: p1Object1 } = p1ObjectsAndBehaviors[0];
|
||
const { object: p1Object2 } = p1ObjectsAndBehaviors[1];
|
||
expect(p1Object1.getX()).to.be(142);
|
||
expect(p1Object1.getY()).to.be(143);
|
||
expect(p1Object2.getX()).to.be(0);
|
||
expect(p1Object2.getY()).to.be(0);
|
||
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
p3RuntimeScene.createObject('MySpriteObject');
|
||
const { behavior: p3MultiplayerBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[1]; // The new instance
|
||
p3MultiplayerBehavior.setPlayerObjectOwnership(2);
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const p3ObjectsAndBehaviors = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
);
|
||
expect(p3ObjectsAndBehaviors.length).to.be(2);
|
||
const { object: p3Object1 } = p3ObjectsAndBehaviors[0];
|
||
const { object: p3Object2 } = p3ObjectsAndBehaviors[1];
|
||
expect(p3Object1.getX()).to.be(142);
|
||
expect(p3Object1.getY()).to.be(143);
|
||
expect(p3Object2.getX()).to.be(0);
|
||
expect(p3Object2.getY()).to.be(0);
|
||
}
|
||
|
||
// Create an instance on player 2, owned by player 2.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
p2RuntimeScene.createObject('MySpriteObject');
|
||
const p2ObjectsAndBehaviors = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
);
|
||
expect(p2ObjectsAndBehaviors.length).to.be(2);
|
||
const { object: p2Object1 } = p2ObjectsAndBehaviors[0];
|
||
const { object: p2Object2, behavior: p2MultiplayerBehavior2 } =
|
||
p2ObjectsAndBehaviors[1];
|
||
expect(p2Object1.getX()).to.be(142);
|
||
expect(p2Object1.getY()).to.be(143);
|
||
expect(p2Object2.getX()).to.be(0);
|
||
expect(p2Object2.getY()).to.be(0);
|
||
p2Object2.setX(42);
|
||
p2Object2.setY(43);
|
||
p2MultiplayerBehavior2.setPlayerObjectOwnership(2);
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Verify the host and player 3 are notified of the new instance, and that they reuse
|
||
// their "ghost" instance for it.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
const p1ObjectsAndBehaviors = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
);
|
||
expect(p1ObjectsAndBehaviors.length).to.be(2); // Initial instance + new instance overriding the ghost
|
||
const { object: p1Object1 } = p1ObjectsAndBehaviors[0];
|
||
const { object: p1Object2 } = p1ObjectsAndBehaviors[1];
|
||
expect(p1Object1.getX()).to.be(142);
|
||
expect(p1Object1.getY()).to.be(143);
|
||
expect(p1Object2.getX()).to.be(42);
|
||
expect(p1Object2.getY()).to.be(43);
|
||
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
const p3ObjectsAndBehaviors = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
);
|
||
expect(p3ObjectsAndBehaviors.length).to.be(2); // Initial instance + new instance overriding the ghost
|
||
const { object: p3Object1 } = p3ObjectsAndBehaviors[0];
|
||
const { object: p3Object2 } = p3ObjectsAndBehaviors[1];
|
||
expect(p3Object1.getX()).to.be(142);
|
||
expect(p3Object1.getY()).to.be(143);
|
||
expect(p3Object2.getX()).to.be(42);
|
||
expect(p3Object2.getY()).to.be(43);
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
}
|
||
});
|
||
|
||
it('deletes an instance owned by another player after a bit (if not "reconciled" in the meantime)', async () => {
|
||
const { switchToPeer, initiateGameWithPlayers } =
|
||
createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
{ playerNumber: 3, peerId: 'player-3' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
// Create an instance on a player (2), owned by another player (3).
|
||
// We can assume it's because there is some common logic running for all players
|
||
// resulting in the creation of this instance everywhere.
|
||
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p2RuntimeScene.createObject('MySpriteObject');
|
||
|
||
const {
|
||
object: mySpriteObject1,
|
||
behavior: p2SpriteMultiplayerObjectBehavior,
|
||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
mySpriteObject1.setX(142);
|
||
mySpriteObject1.setY(143);
|
||
p2SpriteMultiplayerObjectBehavior.setPlayerObjectOwnership(3);
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
const p2Objects = p2RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p2Objects) throw new Error('No object found');
|
||
expect(p2Objects.length).to.be(1);
|
||
|
||
await delay(20);
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
const p2ObjectsUpdated = p2RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p2ObjectsUpdated) throw new Error('No object found');
|
||
expect(p2ObjectsUpdated.length).to.be(1);
|
||
|
||
// After some time, the instance should be deleted as it is owned by another player
|
||
// and was never synchronized since then. Player 3 probably created an instance for a logic
|
||
// that was run too early, or never ran on the other players.
|
||
await delay(500);
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
const p2ObjectsUpdated2 = p2RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p2ObjectsUpdated2) throw new Error('No object found');
|
||
expect(p2ObjectsUpdated2.length).to.be(0);
|
||
});
|
||
|
||
it('deletes an instance owned by another player instantly if not belonging to an existing player', async () => {
|
||
const { switchToPeer, initiateGameWithPlayers } =
|
||
createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
// Create an instance on a player (2), owned by another player (3).
|
||
// We can assume it's because there is some common logic running for all players
|
||
// resulting in the creation of this instance everywhere.
|
||
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p2RuntimeScene.createObject('MySpriteObject');
|
||
|
||
const {
|
||
object: mySpriteObject1,
|
||
behavior: p2SpriteMultiplayerObjectBehavior,
|
||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
mySpriteObject1.setX(142);
|
||
mySpriteObject1.setY(143);
|
||
p2SpriteMultiplayerObjectBehavior.setPlayerObjectOwnership(3);
|
||
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
const p2Objects = p2RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p2Objects) throw new Error('No object found');
|
||
expect(p2Objects.length).to.be(0);
|
||
});
|
||
|
||
it('gives priority to the first ownership change and revert the wrong one', async () => {
|
||
const {
|
||
switchToPeer,
|
||
markAllPeerMessagesAsProcessed,
|
||
initiateGameWithPlayers,
|
||
} = createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
{ playerNumber: 3, peerId: 'player-3' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
// Create an instance on the host's game:
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p1RuntimeScene.createObject('MySpriteObject');
|
||
const {
|
||
object: mySpriteObject1,
|
||
behavior: p1SpriteMultiplayerObjectBehavior,
|
||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
mySpriteObject1.setX(142);
|
||
mySpriteObject1.setY(143);
|
||
// No ownership given, it's owned by the host.
|
||
expect(
|
||
p1SpriteMultiplayerObjectBehavior.getPlayerObjectOwnership()
|
||
).to.be(0);
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Check the object is created on the other players.
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
const p2Objects = p2RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p2Objects) throw new Error('No object found');
|
||
expect(p2Objects.length).to.be(0);
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { object: p2SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p2SpriteObject.getX()).to.be(142);
|
||
expect(p2SpriteObject.getY()).to.be(143);
|
||
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p3RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
|
||
const p3Objects = p3RuntimeScene.getObjects('MySpriteObject');
|
||
if (!p3Objects) throw new Error('No object found');
|
||
expect(p3Objects.length).to.be(0);
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { object: p3SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p3SpriteObject.getX()).to.be(142);
|
||
expect(p3SpriteObject.getY()).to.be(143);
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
|
||
// Now, try to change ownership to player 2 and 3 at the "same time".
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { behavior: p2SpriteMultiplayerObjectBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
p2SpriteMultiplayerObjectBehavior.setPlayerObjectOwnership(2);
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { behavior: p3SpriteMultiplayerObjectBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
p3SpriteMultiplayerObjectBehavior.setPlayerObjectOwnership(3);
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Verify the host honors the first one (ownership change from 0 to 2).
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
const { behavior: p1SpriteMultiplayerObjectBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(
|
||
p1SpriteMultiplayerObjectBehavior.getPlayerObjectOwnership()
|
||
).to.be(0);
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { behavior: p1SpriteMultiplayerObjectBehaviorUpdated } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
expect(
|
||
p1SpriteMultiplayerObjectBehaviorUpdated.getPlayerObjectOwnership()
|
||
).to.be(2);
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
}
|
||
|
||
// Wait so that player 3 retries.
|
||
await delay(210);
|
||
|
||
// Try 4 times and wait for more than 200ms between each try.
|
||
{
|
||
for (let i = 0; i < 4; i++) {
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { behavior: p3SpriteMultiplayerObjectBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
expect(
|
||
p3SpriteMultiplayerObjectBehavior.getPlayerObjectOwnership()
|
||
).to.be(3);
|
||
|
||
markAllPeerMessagesAsProcessed();
|
||
|
||
await delay(210);
|
||
}
|
||
}
|
||
|
||
// Check ownership was reverted.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { behavior: p3SpriteMultiplayerObjectBehavior } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
expect(
|
||
p3SpriteMultiplayerObjectBehavior.getPlayerObjectOwnership()
|
||
).to.be(0);
|
||
markAllPeerMessagesAsProcessed();
|
||
}
|
||
|
||
// Move the object on the player 2:
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { object: p2SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
p2SpriteObject.setX(242);
|
||
p2SpriteObject.setY(243);
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Check the object is moved on the host.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { object: p1SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p1SpriteObject.getX()).to.be(242);
|
||
expect(p1SpriteObject.getY()).to.be(243);
|
||
}
|
||
|
||
// Check the object is moved on the other player.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers,
|
||
});
|
||
p3RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const {
|
||
object: p3SpriteObject,
|
||
behavior: p3SpriteMultiplayerObjectBehavior,
|
||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p3RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
|
||
expect(p3SpriteObject.getX()).to.be(242);
|
||
expect(p3SpriteObject.getY()).to.be(243);
|
||
expect(
|
||
p3SpriteMultiplayerObjectBehavior.getPlayerObjectOwnership()
|
||
).to.be(2);
|
||
}
|
||
markAllPeerMessagesAsProcessed();
|
||
});
|
||
|
||
it('synchronizes object behaviors from the host to other players', async () => {
|
||
const { switchToPeer, initiateGameWithPlayers } =
|
||
createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
// Create an instance on the host's game:
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p1RuntimeScene.createObject('MySpriteObject');
|
||
const { object: p1SpriteObjectOriginal } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
// Ensure dummy behavior is there.
|
||
/** @type {gdjs.DummyRuntimeBehavior | null} */
|
||
// @ts-ignore - We know this returns an DummyRuntimeBehavior
|
||
const p1DummyBehaviorOriginal =
|
||
p1SpriteObjectOriginal.getBehavior('DummyBehavior');
|
||
if (!p1DummyBehaviorOriginal) throw new Error('No dummy behavior found');
|
||
expect(p1DummyBehaviorOriginal._activated).to.be(true);
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Check the object is created on the other peer with the behavior.
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
const { object: p2SpriteObjectOriginal } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
// Ensure dummy behavior is there.
|
||
/** @type {gdjs.DummyRuntimeBehavior | null} */
|
||
// @ts-ignore - We know this returns an DummyRuntimeBehavior
|
||
const p2DummyBehaviorOriginal =
|
||
p2SpriteObjectOriginal.getBehavior('DummyBehavior');
|
||
if (!p2DummyBehaviorOriginal) throw new Error('No dummy behavior found');
|
||
expect(p2DummyBehaviorOriginal._activated).to.be(true);
|
||
|
||
// Deactivate the behavior on the player 2
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { object: p2SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
/** @type {gdjs.DummyRuntimeBehavior | null} */
|
||
// @ts-ignore - We know this returns an DummyRuntimeBehavior
|
||
const p2DummyBehavior = p2SpriteObject.getBehavior('DummyBehavior');
|
||
if (!p2DummyBehavior) throw new Error('No dummy behavior found');
|
||
expect(p2DummyBehavior._activated).to.be(true);
|
||
p2DummyBehavior.activate(false);
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Forward time on player 1, who is host.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
// As the object is not moving, it will not be synced a lot, so we need to wait a bit.
|
||
await delay(20);
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Check the behavior is activated again on player 2, as they do not own the object.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
// As the object is not moving, it will not be synced a lot, so we need to wait a bit.
|
||
await delay(20);
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
const { object: p2SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
/** @type {gdjs.DummyRuntimeBehavior | null} */
|
||
// @ts-ignore - We know this returns an DummyRuntimeBehavior
|
||
const p2DummyBehavior = p2SpriteObject.getBehavior('DummyBehavior');
|
||
if (!p2DummyBehavior) throw new Error('No dummy behavior found');
|
||
expect(p2DummyBehavior._activated).to.be(true);
|
||
}
|
||
|
||
// Deactivate the behavior on the host.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const { object: p1SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
/** @type {gdjs.DummyRuntimeBehavior | null} */
|
||
// @ts-ignore - We know this returns an DummyRuntimeBehavior
|
||
const p1DummyBehavior = p1SpriteObject.getBehavior('DummyBehavior');
|
||
if (!p1DummyBehavior) throw new Error('No dummy behavior found');
|
||
p1DummyBehavior.activate(false);
|
||
// As the object is not moving, it will not be synced a lot, so we need to wait a bit.
|
||
await delay(20);
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
}
|
||
|
||
// Check the behavior is deactivated on player 2, as per the host's decision.
|
||
{
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
// As the object is not moving, it will not be synced a lot, so we need to wait a bit.
|
||
await delay(20);
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
const { object: p2SpriteObject } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
/** @type {gdjs.DummyRuntimeBehavior | null} */
|
||
// @ts-ignore - We know this returns an DummyRuntimeBehavior
|
||
const p2DummyBehavior = p2SpriteObject.getBehavior('DummyBehavior');
|
||
if (!p2DummyBehavior) throw new Error('No dummy behavior found');
|
||
expect(p2DummyBehavior._activated).to.be(false);
|
||
}
|
||
});
|
||
|
||
it('does not synchronize object behaviors if defined as not synchronized', async () => {
|
||
const { switchToPeer, initiateGameWithPlayers } =
|
||
createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
// Create an instance on the host's game:
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p1RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p1RuntimeScene.createObject('MySpriteObject');
|
||
const {
|
||
object: p1SpriteObjectOriginal,
|
||
behavior: p1SpriteMultiplayerObjectBehavior,
|
||
} = getObjectAndMultiplayerBehaviorsFromScene(
|
||
p1RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
// Ensure dummy behavior is there.
|
||
/** @type {gdjs.DummyRuntimeBehavior | null} */
|
||
// @ts-ignore - We know this returns an DummyRuntimeBehavior
|
||
const p1DummyBehaviorOriginal =
|
||
p1SpriteObjectOriginal.getBehavior('DummyBehavior');
|
||
if (!p1DummyBehaviorOriginal) throw new Error('No dummy behavior found');
|
||
expect(p1DummyBehaviorOriginal._activated).to.be(true);
|
||
// Deactivate it and mark it as not synchronized.
|
||
p1DummyBehaviorOriginal.activate(false);
|
||
p1SpriteMultiplayerObjectBehavior.enableBehaviorSynchronization(
|
||
'DummyBehavior',
|
||
false
|
||
);
|
||
|
||
p1RuntimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Check the object is created on the other peer with the behavior.
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
p2RuntimeScene.renderAndStep(1000 / 60);
|
||
const { object: p2SpriteObjectOriginal } =
|
||
getObjectAndMultiplayerBehaviorsFromScene(
|
||
p2RuntimeScene,
|
||
'MySpriteObject'
|
||
)[0];
|
||
// Ensure dummy behavior is there.
|
||
/** @type {gdjs.DummyRuntimeBehavior | null} */
|
||
// @ts-ignore - We know this returns an DummyRuntimeBehavior
|
||
const p2DummyBehaviorOriginal =
|
||
p2SpriteObjectOriginal.getBehavior('DummyBehavior');
|
||
if (!p2DummyBehaviorOriginal) throw new Error('No dummy behavior found');
|
||
|
||
// It is activated as it is not synchronized.
|
||
expect(p2DummyBehaviorOriginal._activated).to.be(true);
|
||
});
|
||
});
|
||
|
||
describe('Multiple scene tests', () => {
|
||
/**
|
||
* @param {gdjs.RuntimeGame} runtimeGame
|
||
* @param {string} expectedSceneName
|
||
* @returns {{currentScene: gdjs.RuntimeScene}}
|
||
*/
|
||
const checkCurrentSceneIs = (runtimeGame, expectedSceneName) => {
|
||
const currentScene = runtimeGame.getSceneStack().getCurrentScene();
|
||
if (!currentScene) throw new Error('No current scene found.');
|
||
expect(currentScene.getName()).to.be(expectedSceneName);
|
||
|
||
return { currentScene };
|
||
};
|
||
|
||
it('synchronizes scenes from the host to other players', async () => {
|
||
const { switchToPeer, initiateGameWithPlayers } =
|
||
createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
const gameLayoutData = [
|
||
getFakeSceneAndExtensionData({ name: 'Scene1' }).sceneData,
|
||
getFakeSceneAndExtensionData({ name: 'Scene2' }).sceneData,
|
||
getFakeSceneAndExtensionData({ name: 'Scene3' }).sceneData,
|
||
];
|
||
|
||
// Launch two games.
|
||
const p1RuntimeGame = gdjs.getPixiRuntimeGame({
|
||
layouts: gameLayoutData,
|
||
});
|
||
|
||
await p1RuntimeGame._resourcesLoader.loadAllResources(() => {});
|
||
|
||
const p2RuntimeGame = gdjs.getPixiRuntimeGame({
|
||
layouts: gameLayoutData,
|
||
});
|
||
|
||
await p2RuntimeGame._resourcesLoader.loadAllResources(() => {});
|
||
|
||
// Launch two scenes on the host:
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p1RuntimeGame.getSceneStack().push('Scene1');
|
||
p1RuntimeGame.getSceneStack().push('Scene3');
|
||
|
||
checkCurrentSceneIs(p1RuntimeGame, 'Scene3');
|
||
p1RuntimeGame.getSceneStack().step(1000 / 60);
|
||
|
||
// Launch the game on a client, with just the first scene.
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p2RuntimeGame.getSceneStack().push('Scene1');
|
||
|
||
// Ensure the second scene (Scene3) is started.
|
||
checkCurrentSceneIs(p2RuntimeGame, 'Scene1');
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
checkCurrentSceneIs(p2RuntimeGame, 'Scene3');
|
||
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
|
||
// Start again the same scene (Scene3) on the host's game
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
let p1FirstScene3NetworkId;
|
||
checkCurrentSceneIs(p1RuntimeGame, 'Scene3');
|
||
{
|
||
p1RuntimeGame.getSceneStack().step(1000 / 60);
|
||
const { currentScene } = checkCurrentSceneIs(p1RuntimeGame, 'Scene3');
|
||
p1FirstScene3NetworkId = currentScene.networkId;
|
||
}
|
||
|
||
p1RuntimeGame.getSceneStack().push('Scene3');
|
||
let p1SecondScene3NetworkId;
|
||
{
|
||
p1RuntimeGame.getSceneStack().step(1000 / 60);
|
||
const { currentScene } = checkCurrentSceneIs(p1RuntimeGame, 'Scene3');
|
||
p1SecondScene3NetworkId = currentScene.networkId;
|
||
}
|
||
|
||
expect(p1FirstScene3NetworkId).not.to.be(null);
|
||
expect(p1SecondScene3NetworkId).not.to.be(null);
|
||
expect(p1FirstScene3NetworkId).not.to.be(p1SecondScene3NetworkId);
|
||
|
||
// Ensure the second Scene3 is also started on the player.
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
{
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
const { currentScene } = checkCurrentSceneIs(p2RuntimeGame, 'Scene3');
|
||
expect(currentScene.networkId).to.be(p1FirstScene3NetworkId);
|
||
}
|
||
{
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
const { currentScene } = checkCurrentSceneIs(p2RuntimeGame, 'Scene3');
|
||
expect(currentScene.networkId).to.be(p1SecondScene3NetworkId);
|
||
}
|
||
|
||
// Remove the two Scene3 on the host's game.
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
p1RuntimeGame.getSceneStack().pop();
|
||
p1RuntimeGame.getSceneStack().pop();
|
||
|
||
let p1Scene1NetworkId;
|
||
{
|
||
p1RuntimeGame.getSceneStack().step(1000 / 60);
|
||
const { currentScene } = checkCurrentSceneIs(p1RuntimeGame, 'Scene1');
|
||
p1Scene1NetworkId = currentScene.networkId;
|
||
}
|
||
|
||
// Check that the player also goes back to Scene1:
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
checkCurrentSceneIs(p2RuntimeGame, 'Scene3');
|
||
{
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
const { currentScene } = checkCurrentSceneIs(p2RuntimeGame, 'Scene1');
|
||
expect(currentScene.networkId).to.be(p1Scene1NetworkId);
|
||
}
|
||
});
|
||
|
||
it('reconciles a scene launched both by the host and by a player', async () => {
|
||
const {
|
||
switchToPeer,
|
||
markAllPeerMessagesAsProcessed,
|
||
initiateGameWithPlayers,
|
||
} = createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
|
||
const gameLayoutData = [
|
||
getFakeSceneAndExtensionData({ name: 'Scene1' }).sceneData,
|
||
getFakeSceneAndExtensionData({ name: 'Scene2' }).sceneData,
|
||
getFakeSceneAndExtensionData({ name: 'Scene3' }).sceneData,
|
||
];
|
||
|
||
// Launch two games.
|
||
const p1RuntimeGame = gdjs.getPixiRuntimeGame({
|
||
layouts: gameLayoutData,
|
||
});
|
||
|
||
await p1RuntimeGame._resourcesLoader.loadAllResources(() => {});
|
||
|
||
const p2RuntimeGame = gdjs.getPixiRuntimeGame({
|
||
layouts: gameLayoutData,
|
||
});
|
||
|
||
await p2RuntimeGame._resourcesLoader.loadAllResources(() => {});
|
||
|
||
// Launch two scenes on the host's game:
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p1RuntimeGame.getSceneStack().push('Scene1');
|
||
p1RuntimeGame.getSceneStack().step(1000 / 60);
|
||
|
||
checkCurrentSceneIs(p1RuntimeGame, 'Scene1');
|
||
|
||
// Launch the game on a client, with just the first scene.
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p2RuntimeGame.getSceneStack().push('Scene1');
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
|
||
checkCurrentSceneIs(p2RuntimeGame, 'Scene1');
|
||
markAllPeerMessagesAsProcessed();
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
|
||
// Launch a second scene, first on the player:
|
||
p2RuntimeGame.getSceneStack().push('Scene2');
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
|
||
expect(
|
||
checkCurrentSceneIs(p2RuntimeGame, 'Scene2').currentScene.networkId
|
||
).to.be(null);
|
||
|
||
// Launch a second scene, this time on the host's game:
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
p1RuntimeGame.getSceneStack().push('Scene2');
|
||
p1RuntimeGame.getSceneStack().step(1000 / 60);
|
||
|
||
const p1Scene2NetworkId = checkCurrentSceneIs(p1RuntimeGame, 'Scene2')
|
||
.currentScene.networkId;
|
||
|
||
// Check the network id of the scene on the player is reconciled with the host.
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers,
|
||
});
|
||
|
||
expect(
|
||
checkCurrentSceneIs(p2RuntimeGame, 'Scene2').currentScene.networkId
|
||
).to.be(null);
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
p2RuntimeGame.getSceneStack().step(1000 / 60);
|
||
|
||
expect(
|
||
checkCurrentSceneIs(p2RuntimeGame, 'Scene2').currentScene.networkId
|
||
).to.be(p1Scene2NetworkId);
|
||
});
|
||
});
|
||
|
||
describe('Player ping', () => {
|
||
it('updates player pings on non-host clients after receiving heartbeats from the host', async () => {
|
||
const {
|
||
switchToPeer,
|
||
initiateGameWithPlayers,
|
||
markAllPeerMessagesAsProcessed,
|
||
} = createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
];
|
||
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
const runtimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
|
||
// Heartbeats are sent every second, so wait for the interval to pass.
|
||
await delay(1100);
|
||
|
||
// Host sends heartbeat; non-host receives and responds immediately.
|
||
switchToPeer({ playerNumber: 1, allConnectedPlayers });
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
|
||
switchToPeer({ playerNumber: 2, allConnectedPlayers });
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Wait so the host measures a non-trivial RTT for player 2.
|
||
await delay(20);
|
||
|
||
// Host processes the response and updates player 2's ping.
|
||
switchToPeer({ playerNumber: 1, allConnectedPlayers });
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Record the ping the host computed for player 2 (RTT >= 20ms, so ping > 0).
|
||
const hostPingForP2 = gdjs.multiplayerMessageManager.getPlayerPing(2);
|
||
expect(hostPingForP2 > 0).to.be(true);
|
||
|
||
// Clear all accumulated messages so that the second heartbeat cycle starts clean,
|
||
// avoiding stale messages being reprocessed across frames.
|
||
markAllPeerMessagesAsProcessed();
|
||
|
||
// Wait for the next heartbeat interval so the host can broadcast the updated pings.
|
||
await delay(1100);
|
||
|
||
// Host broadcasts the updated pings to all non-host players.
|
||
switchToPeer({ playerNumber: 1, allConnectedPlayers });
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Non-host receives the updated heartbeat.
|
||
switchToPeer({ playerNumber: 2, allConnectedPlayers });
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Non-host should now reflect the same ping value the host computed, not
|
||
// the stale value from the initial setup.
|
||
const nonHostPingForP2 = gdjs.multiplayerMessageManager.getPlayerPing(2);
|
||
expect(nonHostPingForP2).to.be(hostPingForP2);
|
||
});
|
||
});
|
||
|
||
describe('Player joins and leaves', () => {
|
||
it('detects a player leaving and send it to other players', async () => {
|
||
const { switchToPeer, initiateGameWithPlayers } =
|
||
createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
{ playerNumber: 3, peerId: 'player-3' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
const runtimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
|
||
// Player 2 leaves.
|
||
const newConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 3, peerId: 'player-3' },
|
||
];
|
||
// Host sees the player 2 leaving.
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers: newConnectedPlayers,
|
||
justDisconnectedPeers: ['player-2'],
|
||
});
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Player 3 sees the player 2 leaving, via the heartbeat from the host.
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers: newConnectedPlayers,
|
||
});
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
const p2PlayersLeft =
|
||
gdjs.multiplayerMessageManager.getPlayersWhoJustLeft();
|
||
expect(p2PlayersLeft).to.be.eql([2]);
|
||
});
|
||
it('detects a player joining and send it to other players', async () => {
|
||
const { switchToPeer, initiateGameWithPlayers } =
|
||
createMultiplayerManagersMock();
|
||
|
||
const allConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 3, peerId: 'player-3' },
|
||
];
|
||
initiateGameWithPlayers(allConnectedPlayers);
|
||
const runtimeScene = makeTestRuntimeSceneWithNetworkId();
|
||
|
||
// Heartbeats are sent every second, so wait for a bit.
|
||
await delay(1000);
|
||
|
||
// Player 2 joins.
|
||
const newConnectedPlayers = [
|
||
{ playerNumber: 1, peerId: 'player-1', isHost: true },
|
||
{ playerNumber: 2, peerId: 'player-2' },
|
||
{ playerNumber: 3, peerId: 'player-3' },
|
||
];
|
||
|
||
// Host sees the player 2 joining and send them a heartbeat.
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers: newConnectedPlayers,
|
||
});
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Player 2 receives the heartbeat and respond.
|
||
switchToPeer({
|
||
playerNumber: 2,
|
||
allConnectedPlayers: newConnectedPlayers,
|
||
});
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
|
||
// Host receives player 2 response and send them back their ping.
|
||
switchToPeer({
|
||
playerNumber: 1,
|
||
allConnectedPlayers: newConnectedPlayers,
|
||
});
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
const p1PlayersJoined =
|
||
gdjs.multiplayerMessageManager.getPlayersWhoJustJoined();
|
||
expect(p1PlayersJoined).to.be.eql([2]);
|
||
|
||
// Player 3 also sees the player 2 joining.
|
||
switchToPeer({
|
||
playerNumber: 3,
|
||
allConnectedPlayers: newConnectedPlayers,
|
||
});
|
||
runtimeScene.renderAndStep(1000 / 60);
|
||
const p3PlayersJoined =
|
||
gdjs.multiplayerMessageManager.getPlayersWhoJustJoined();
|
||
expect(p3PlayersJoined).to.be.eql([2]);
|
||
});
|
||
});
|
||
});
|