Compare commits

...

1 Commits

Author SHA1 Message Date
Clément Pasteau
625fef0d39 Make Multiplayer extension independent from p2p extension + add message compression 2024-06-26 16:08:01 +02:00
10 changed files with 1850 additions and 1001 deletions

View File

@@ -46,8 +46,8 @@ module.exports = {
.addCodeOnlyParameter('currentScene', '')
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -74,8 +74,8 @@ module.exports = {
.addParameter('yesorno', _('Show close button'), '', false)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -102,8 +102,8 @@ module.exports = {
)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -132,8 +132,8 @@ module.exports = {
.addParameter('string', _('Message name'), '', false)
.addParameter('string', _('Message content'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -162,8 +162,8 @@ module.exports = {
.addParameter('string', _('Message name'), '', false)
.addParameter('variable', _('Variable'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -194,8 +194,8 @@ module.exports = {
.addParameter('string', _('Message name'), '', false)
.addParameter('variable', _('Variable'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -222,8 +222,8 @@ module.exports = {
)
.addCodeOnlyParameter('currentScene', '')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -247,8 +247,8 @@ module.exports = {
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -272,8 +272,8 @@ module.exports = {
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -297,8 +297,8 @@ module.exports = {
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -325,8 +325,8 @@ module.exports = {
)
.addParameter('string', _('Message name'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -352,8 +352,8 @@ module.exports = {
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -377,8 +377,8 @@ module.exports = {
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -403,8 +403,8 @@ module.exports = {
)
.getCodeExtraInformation()
.addParameter('number', _('Player number'), '', false)
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -429,8 +429,8 @@ module.exports = {
)
.addParameter('string', _('Message name'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -453,8 +453,8 @@ module.exports = {
)
.addParameter('string', _('Message name'), '', false)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -477,8 +477,8 @@ module.exports = {
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -502,8 +502,8 @@ module.exports = {
_('Lobbies'),
'JsPlatform/Extensions/multiplayer.svg'
)
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -532,8 +532,8 @@ module.exports = {
false
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -555,8 +555,8 @@ module.exports = {
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -584,8 +584,8 @@ module.exports = {
false
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -607,8 +607,8 @@ module.exports = {
'JsPlatform/Extensions/multiplayer.svg'
)
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -637,8 +637,8 @@ module.exports = {
'number',
gd.ParameterOptions.makeNewOptions().setDescription(_('Player number'))
)
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -669,8 +669,8 @@ module.exports = {
.addParameter('variable', _('Variable'), '', false)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -700,8 +700,8 @@ module.exports = {
.addParameter('variable', _('Variable'), '', false)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -731,8 +731,8 @@ module.exports = {
.addParameter('variable', _('Variable'), '', false)
.setHelpPath('/all-features/multiplayer')
.getCodeExtraInformation()
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
@@ -837,8 +837,8 @@ module.exports = {
multiplayerObjectBehavior,
sharedData
)
.setIncludeFile('Extensions/P2P/A_peer.js')
.addIncludeFile('Extensions/P2P/B_p2ptools.js')
.setIncludeFile('Extensions/Multiplayer/peer.js')
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)

File diff suppressed because it is too large Load Diff

View File

@@ -228,7 +228,7 @@ namespace gdjs {
// Before sending the change owner message, if we are becoming the new owner,
// we want to ensure this message is acknowledged, by everyone we're connected to.
if (variableData.newVariableOwner === currentPlayerNumber) {
const otherPeerIds = gdjs.evtTools.p2p.getAllPeers();
const otherPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const variableOwnerChangedMessageName = gdjs.multiplayerMessageManager.createVariableOwnerChangedMessageNameFromChangeVariableOwnerMessage(
messageName
);
@@ -243,14 +243,12 @@ namespace gdjs {
}
debugLogger.info('Sending change owner message', messageName);
const connectedPeerIds = gdjs.evtTools.p2p.getAllPeers();
for (const peerId of connectedPeerIds) {
gdjs.multiplayerMessageManager.sendDataTo(
peerId,
messageName,
messageData
);
}
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
gdjs.multiplayerMessageManager.sendDataTo(
connectedPeerIds,
messageName,
messageData
);
// Remove the variable from the list of variables ownership changes to sync.
delete variableOwnershipChangesToSyncAtEndOfFrame[variableNetworkId];

View File

@@ -101,17 +101,19 @@ namespace gdjs {
}, this._timeBeforeDestroyingObjectWithoutNetworkIdInMs);
}
private _sendDataToPeersWithIncreasedClock(
private _sendDataToPeersWithIncreasedClock = async (
messageName: string,
data: Object
) {
) => {
this._clock++;
data['_clock'] = this._clock;
const connectedPeerIds = gdjs.evtTools.p2p.getAllPeers();
for (const peerId of connectedPeerIds) {
gdjs.multiplayerMessageManager.sendDataTo(peerId, messageName, data);
}
}
const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
await gdjs.multiplayerMessageManager.sendDataTo(
connectedPeerIds,
messageName,
data
);
};
private _isOwnerAsPlayerOrHost() {
const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();
@@ -453,7 +455,7 @@ namespace gdjs {
// If we are player 1, we are connected to everyone, so we expect an acknowledgment from everyone.
// If we are another player, we are only connected to player 1, so we expect an acknowledgment from player 1.
// In both cases, this represents the list of peers the current user is connected to.
const otherPeerIds = gdjs.evtTools.p2p.getAllPeers();
const otherPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const {
messageName: destroyMessageName,
messageData: destroyMessageData,
@@ -563,7 +565,7 @@ namespace gdjs {
// If we are another player, we are only connected to player 1, so we expect an acknowledgment from player 1.
// In both cases, this represents the list of peers the current user is connected to.
if (newObjectPlayerNumber === currentPlayerNumber) {
const otherPeerIds = gdjs.evtTools.p2p.getAllPeers();
const otherPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();
const changeOwnerAcknowledgedMessageName = gdjs.multiplayerMessageManager.createInstanceOwnerChangedMessageNameFromChangeInstanceOwnerMessage(
messageName
);
@@ -586,6 +588,9 @@ namespace gdjs {
// If we are the new owner, also send directly an update of the position,
// so that the object is immediately moved on the screen and we don't wait for the next tick.
if (newObjectPlayerNumber === currentPlayerNumber) {
debugLogger.info(
'Sending update message to move the object immediately.'
);
const objectNetworkSyncData = this.owner.getNetworkSyncData();
const {
messageName: updateMessageName,

View File

@@ -562,7 +562,7 @@ namespace gdjs {
// When the connectionId is received, initialise PeerJS so players can connect to each others afterwards.
if (validIceServers.length) {
for (const server of validIceServers) {
gdjs.evtTools.p2p.useCustomICECandidate(
gdjs.multiplayerPeerJsHelper.useCustomICECandidate(
server.urls,
server.username,
server.credential
@@ -570,7 +570,7 @@ namespace gdjs {
}
}
if (brokerServerConfig) {
gdjs.evtTools.p2p.useCustomBrokerServer(
gdjs.multiplayerPeerJsHelper.useCustomBrokerServer(
brokerServerConfig.hostname,
brokerServerConfig.port,
brokerServerConfig.path,
@@ -578,7 +578,7 @@ namespace gdjs {
brokerServerConfig.secure
);
} else {
gdjs.evtTools.p2p.useDefaultBrokerServer();
gdjs.multiplayerPeerJsHelper.useDefaultBrokerServer();
}
_connectionId = connectionId;
@@ -705,7 +705,7 @@ namespace gdjs {
// It is possible the connection to other players didn't work.
// If that's the case, show an error message and leave the lobby.
// If we are the host, still start the game, as this allows a player to test the game alone.
const allConnectedPeers = gdjs.evtTools.p2p.getAllPeers();
const allConnectedPeers = gdjs.multiplayerPeerJsHelper.getAllPeers();
if (!isPlayerHost() && allConnectedPeers.length === 0) {
gdjs.multiplayerComponents.displayConnectionErrorNotification(
runtimeScene
@@ -775,7 +775,7 @@ namespace gdjs {
}
// Disconnect from any P2P connections.
gdjs.evtTools.p2p.disconnectFromAllPeers();
gdjs.multiplayerPeerJsHelper.disconnectFromAllPeers();
// Clear the expected acknowledgments, as the game is ending.
gdjs.multiplayerMessageManager.clearExpectedMessageAcknowledgements();
@@ -787,7 +787,7 @@ namespace gdjs {
*/
const handlePeerIdEvent = function (peerId: string) {
// When a peerId is received, trigger a P2P connection with the peer.
const currentPeerId = gdjs.evtTools.p2p.getCurrentId();
const currentPeerId = gdjs.multiplayerPeerJsHelper.getCurrentId();
if (!currentPeerId) {
logger.error(
'No peerId found, the player does not seem connected to the broker server.'
@@ -800,7 +800,7 @@ namespace gdjs {
return;
}
gdjs.evtTools.p2p.connect(peerId);
gdjs.multiplayerPeerJsHelper.connect(peerId);
};
/**
@@ -862,6 +862,8 @@ namespace gdjs {
// Consider the game is ended, so that we don't listen to other players disconnecting.
_isLobbyGameRunning = false;
logger.info('Ending the lobby game.');
// Inform the players that the game has ended.
gdjs.multiplayerMessageManager.sendEndGameMessage();
@@ -912,7 +914,7 @@ namespace gdjs {
return;
}
const peerId = gdjs.evtTools.p2p.getCurrentId();
const peerId = gdjs.multiplayerPeerJsHelper.getCurrentId();
if (!peerId) {
logger.error(
"No peerId found, the player doesn't seem connected to the broker server."

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,414 @@
/// <reference path="peerjs.d.ts" />
namespace gdjs {
const logger = new gdjs.Logger('Multiplayer');
export namespace multiplayerPeerJsHelper {
/**
* The type of the data that is sent across peerjs.
* We use UInt8Array to send compressed data, but we only manipulate objects once received.
*/
type NetworkMessage = {
messageName: string;
data: Uint8Array;
};
/**
* Helper to discard invalid messages when received.
*/
const isValidNetworkMessage = (
message: unknown
): message is NetworkMessage =>
typeof message === 'object' &&
message !== null &&
typeof message['messageName'] === 'string' &&
typeof message['data'] === 'object';
export interface IMessageData {
readonly data: any; // The data sent with the message, an object with unknown content.
readonly sender: String;
getData(): any;
getSender(): string;
}
/**
* The data bound to a message name.
*/
export class MessageData implements IMessageData {
public readonly data: any;
public readonly sender: string;
constructor(data: object, sender: string) {
this.data = data;
this.sender = sender;
}
public getData(): any {
return this.data;
}
public getSender(): string {
return this.sender;
}
}
export interface IMessagesList {
getName(): string;
getMessages(): IMessageData[];
pushMessage(data: object, sender: string): void;
}
export class MessagesList implements IMessagesList {
private readonly data: IMessageData[] = [];
private readonly messageName: string;
constructor(messageName: string) {
this.messageName = messageName;
}
public getName(): string {
return this.messageName;
}
public getMessages(): IMessageData[] {
return this.data;
}
public pushMessage(data: object, sender: string): void {
this.data.push(new MessageData(data, sender));
}
}
/**
* The peer to peer configuration.
*/
let peerConfig: Peer.PeerJSOption = { debug: 1 };
/**
* The p2p client.
*/
let peer: Peer<NetworkMessage> | null = null;
/**
* All connected p2p clients, keyed by their ID.
*/
const connections = new Map<string, Peer.DataConnection<NetworkMessage>>();
/**
* Contains a map of message triggered by other p2p clients.
* It is keyed by the event name.
*/
const allMessages = new Map<string, IMessagesList>();
/**
* True if PeerJS is initialized and ready.
*/
let ready = false;
/**
* List of IDs of peers that just disconnected.
*/
const justDisconnectedPeers: string[] = [];
/**
* List of IDs of peers that just remotely initiated a connection.
*/
const justConnectedPeers: string[] = [];
/**
* Helper function to compress data sent over the network.
*/
async function compressData(data: object): Promise<Uint8Array> {
const jsonString = JSON.stringify(data); // Convert object to JSON string
const encoder = new TextEncoder();
const array = encoder.encode(jsonString); // Convert string to Uint8Array
// @ts-ignore - As of 09/2023 the CompressionStream is now available in all browsers.
const cs = new CompressionStream('gzip'); // Create a CompressionStream with gzip
const writer = cs.writable.getWriter();
writer.write(array);
writer.close();
const compressedStream = cs.readable;
const reader = compressedStream.getReader();
const chunks: any[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const compressedData = new Uint8Array(
chunks.reduce((acc, chunk) => acc.concat(Array.from(chunk)), [])
);
return compressedData;
}
/**
* Helper function to decompress data received over the network.
* It returns the parsed JSON object, if valid, or undefined.
*/
async function decompressData(
compressedData: Uint8Array
): Promise<object | undefined> {
// @ts-ignore - As of 09/2023 the DecompressionStream is now available in all browsers.
const ds = new DecompressionStream('gzip'); // Create a DecompressionStream with gzip
const writer = ds.writable.getWriter();
writer.write(compressedData);
writer.close();
const decompressedStream = ds.readable;
const reader = decompressedStream.getReader();
const chunks: any[] = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
const decompressedData = new Uint8Array(
chunks.reduce((acc, chunk) => acc.concat(Array.from(chunk)), [])
);
const decoder = new TextDecoder();
const jsonStringData = decoder.decode(decompressedData); // Convert Uint8Array back to string
try {
const parsedData = JSON.parse(jsonStringData);
return parsedData;
} catch (e) {
logger.error(`Error while parsing message: ${e.toString()}`);
return;
}
}
/**
* Helper function to get the messages list for a given message name.
*/
export const getOrCreateMessagesList = (
messageName: string
): IMessagesList => {
const messagesList = allMessages.get(messageName);
if (messagesList) return messagesList;
const newMessagesList = new MessagesList(messageName);
allMessages.set(messageName, newMessagesList);
return newMessagesList;
};
/**
* Internal function called when a connection with a remote peer is initiated.
* @param connection The DataConnection of the peer
*/
const _onConnect = (connection: Peer.DataConnection<NetworkMessage>) => {
connections.set(connection.peer, connection);
connection.on('data', async (data) => {
if (isValidNetworkMessage(data)) {
const messagesList = getOrCreateMessagesList(data.messageName);
const messageSender = connection.peer;
const decompressedData = await decompressData(data.data);
if (!decompressedData) return;
messagesList.pushMessage(decompressedData, messageSender);
}
});
// Close event is only for graceful disconnection,
// but we want onDisconnect to trigger for any type of disconnection,
// so we register a listener for both event types.
connection.on('error', () => {
_onDisconnect(connection.peer);
});
connection.on('close', () => {
_onDisconnect(connection.peer);
});
// Regularly check for disconnection as the built in way is not reliable.
(function disconnectChecker() {
if (
connection.peerConnection &&
(connection.peerConnection.connectionState === 'failed' ||
connection.peerConnection.connectionState === 'disconnected' ||
connection.peerConnection.connectionState === 'closed')
) {
_onDisconnect(connection.peer);
} else {
setTimeout(disconnectChecker, 1000);
}
})();
};
/**
* Internal function called when a remote client disconnects.
* @param connectionID The ID of the peer that disconnected.
*/
const _onDisconnect = (connectionID: string) => {
if (!connections.has(connectionID)) return;
justDisconnectedPeers.push(connectionID);
connections.delete(connectionID);
};
/**
* Internal function called to initialize PeerJS after it
* has been configured.
*/
const loadPeerJS = () => {
if (peer !== null) return;
peer = new Peer(peerConfig);
peer.on('open', () => {
ready = true;
});
peer.on('error', (errorMessage) => {
logger.error('PeerJS error:', errorMessage);
});
peer.on('connection', (connection) => {
connection.on('open', () => {
_onConnect(connection);
justConnectedPeers.push(connection.peer);
});
});
peer.on('close', () => {
peer = null;
loadPeerJS();
});
peer.on('disconnected', peer.reconnect);
};
/**
* Connects to another p2p client.
* @param id - The other client's ID.
*/
export const connect = (id: string) => {
if (peer === null) return;
const connection = peer.connect(id);
connection.on('open', () => {
_onConnect(connection);
});
};
/**
* Disconnects from all other p2p clients.
*/
export const disconnectFromAllPeers = () => {
for (const connection of connections.values()) connection.close();
};
/**
* Send a message to a specific peer.
* @param ids - The IDs of the clients to send the event to.
* @param messageName - The event to trigger.
* @param eventData - Additional data to send with the event.
*/
export const sendDataTo = async (
ids: string[],
messageName: string,
messageData: object
) => {
if (!ids.length) return;
const compressedData = await compressData(messageData);
for (const id of ids) {
const connection = connections.get(id);
if (connection) {
connection.send({
messageName,
data: compressedData,
});
}
}
};
export const getAllMessagesMap = () => allMessages;
/**
* Connects to a custom broker server.
* @param host The host of the broker server.
* @param port The port of the broker server.
* @param path The path (part of the url after the host) to the broker server.
* @param key Optional password to connect to the broker server.
* @param ssl Use ssl?
*/
export const useCustomBrokerServer = (
host: string,
port: number,
path: string,
key: string,
ssl: boolean
) => {
Object.assign(peerConfig, {
host,
port,
path,
secure: ssl,
// All servers have "peerjs" as default key
key: key.length === 0 ? 'peerjs' : key,
});
loadPeerJS();
};
export const useDefaultBrokerServer = loadPeerJS;
/**
* Adds an ICE server candidate, and removes the default ones provided by PeerJs. Must be called before connecting to a broker.
* @param urls The URL of the STUN/TURN server.
* @param username An optional username to send to the server.
* @param credential An optional password to send to the server.
*/
export const useCustomICECandidate = (
urls: string,
username?: string,
credential?: string
) => {
peerConfig.config = peerConfig.config || {};
peerConfig.config.iceServers = peerConfig.config.iceServers || [];
peerConfig.config.iceServers.push({
urls,
username,
credential,
});
};
/**
* Forces the usage of a relay (TURN) server, to avoid sharing IP addresses with the other peers.
* @param shouldUseRelayServer Whether relay-only should be enabled or disabled.
*/
export const forceUseRelayServer = (shouldUseRelayServer: boolean) => {
peerConfig.config = peerConfig.config || {};
peerConfig.config.iceTransportPolicy = shouldUseRelayServer
? 'relay'
: 'all';
};
/**
* Returns the own current peer ID.
* @see Peer.id
*/
export const getCurrentId = (): string => {
if (peer == undefined) return '';
return peer.id || '';
};
/**
* Returns true once PeerJS finished initialization.
* @see ready
*/
export const isReady = () => ready;
/**
* Return any disconnected peers.
*/
export const getJustDisconnectedPeers = () => justDisconnectedPeers;
/**
* Returns the list of all currently connected peers.
*/
export const getAllPeers = () => Array.from(connections.keys());
gdjs.callbacksRuntimeScenePostEvents.push(() => {
// Clear the list of messages at the end of the frame, assuming they've been all processed.
for (const messagesList of allMessages.values()) {
messagesList.getMessages().length = 0;
}
// Clear the list of just connected and disconnected peers.
if (justDisconnectedPeers.length > 0) {
justDisconnectedPeers.length = 0;
}
if (justConnectedPeers.length > 0) {
justConnectedPeers.length = 0;
}
});
}
}

509
Extensions/Multiplayer/peerjs.d.ts vendored Normal file
View File

@@ -0,0 +1,509 @@
/**
* Minimal `EventEmitter` interface that is molded against the Node.js
* `EventEmitter` interface.
*/
declare class EventEmitter<
EventTypes extends EventEmitter.ValidEventTypes = string | symbol,
Context extends any = any
> {
static prefixed: string | boolean;
/**
* Return an array listing the events for which the emitter has registered
* listeners.
*/
eventNames(): Array<EventEmitter.EventNames<EventTypes>>;
/**
* Return the listeners registered for a given event.
*/
listeners<T extends EventEmitter.EventNames<EventTypes>>(
event: T
): Array<EventEmitter.EventListener<EventTypes, T>>;
/**
* Return the number of listeners listening to a given event.
*/
listenerCount(event: EventEmitter.EventNames<EventTypes>): number;
/**
* Calls each of the listeners registered for a given event.
*/
emit<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
...args: EventEmitter.EventArgs<EventTypes, T>
): boolean;
/**
* Add a listener for a given event.
*/
on<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
fn: EventEmitter.EventListener<EventTypes, T>,
context?: Context
): this;
addListener<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
fn: EventEmitter.EventListener<EventTypes, T>,
context?: Context
): this;
/**
* Add a one-time listener for a given event.
*/
once<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
fn: EventEmitter.EventListener<EventTypes, T>,
context?: Context
): this;
/**
* Remove the listeners of a given event.
*/
removeListener<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
fn?: EventEmitter.EventListener<EventTypes, T>,
context?: Context,
once?: boolean
): this;
off<T extends EventEmitter.EventNames<EventTypes>>(
event: T,
fn?: EventEmitter.EventListener<EventTypes, T>,
context?: Context,
once?: boolean
): this;
/**
* Remove all listeners, or those of the specified event.
*/
removeAllListeners(event?: EventEmitter.EventNames<EventTypes>): this;
}
declare namespace EventEmitter {
export interface ListenerFn<Args extends any[] = any[]> {
(...args: Args): void;
}
export interface EventEmitterStatic {
new <
EventTypes extends ValidEventTypes = string | symbol,
Context = any
>(): EventEmitter<EventTypes, Context>;
}
/**
* `object` should be in either of the following forms:
* ```
* interface EventTypes {
* 'event-with-parameters': any[]
* 'event-with-example-handler': (...args: any[]) => void
* }
* ```
*/
export type ValidEventTypes = string | symbol | object;
export type EventNames<T extends ValidEventTypes> = T extends string | symbol
? T
: keyof T;
export type ArgumentMap<T extends object> = {
[K in keyof T]: T[K] extends (...args: any[]) => void
? Parameters<T[K]>
: T[K] extends any[]
? T[K]
: any[];
};
export type EventListener<
T extends ValidEventTypes,
K extends EventNames<T>
> = T extends string | symbol
? (...args: any[]) => void
: (
...args: ArgumentMap<Exclude<T, string | symbol>>[Extract<K, keyof T>]
) => void;
export type EventArgs<
T extends ValidEventTypes,
K extends EventNames<T>
> = Parameters<EventListener<T, K>>;
export const EventEmitter: EventEmitterStatic;
}
declare namespace Peer {
export interface UtilSupportsObj {
browser: boolean;
webRTC: boolean;
audioVideo: boolean;
data: boolean;
binaryBlob: boolean;
reliable: boolean;
}
class Util {
noop(): void;
readonly CLOUD_HOST = '0.peerjs.com';
readonly CLOUD_PORT = 443;
readonly chunkedBrowsers: {
Chrome: number;
chrome: number;
};
readonly chunkedMTU = 16300;
readonly defaultConfig: {
iceServers: (
| {
urls: string;
username?: undefined;
credential?: undefined;
}
| {
urls: string[];
username: string;
credential: string;
}
)[];
sdpSemantics: string;
};
readonly browser: string;
readonly browserVersion: number;
readonly supports: UtilSupportsObj;
validateId(id: string): boolean;
pack: any;
unpack: any;
chunk(
blob: Blob
): {
__peerData: number;
n: number;
total: number;
data: Blob;
}[];
blobToArrayBuffer(
blob: Blob,
cb: (arg: ArrayBuffer | null) => void
): FileReader;
binaryStringToArrayBuffer(binary: string): ArrayBuffer | SharedArrayBuffer;
randomToken(): string;
isSecure(): boolean;
}
export const util: Util;
export enum LogLevel {
Disabled = 0,
Errors = 1,
Warnings = 2,
All = 3,
}
export enum ConnectionType {
Data = 'data',
Media = 'media',
}
export enum PeerErrorType {
BrowserIncompatible = 'browser-incompatible',
Disconnected = 'disconnected',
InvalidID = 'invalid-id',
InvalidKey = 'invalid-key',
Network = 'network',
PeerUnavailable = 'peer-unavailable',
SslUnavailable = 'ssl-unavailable',
ServerError = 'server-error',
SocketError = 'socket-error',
SocketClosed = 'socket-closed',
UnavailableID = 'unavailable-id',
WebRTC = 'webrtc',
}
export enum SerializationType {
Binary = 'binary',
BinaryUTF8 = 'binary-utf8',
JSON = 'json',
}
export enum SocketEventType {
Message = 'message',
Disconnected = 'disconnected',
Error = 'error',
Close = 'close',
}
export enum ServerMessageType {
Heartbeat = 'HEARTBEAT',
Candidate = 'CANDIDATE',
Offer = 'OFFER',
Answer = 'ANSWER',
Open = 'OPEN',
Error = 'ERROR',
IdTaken = 'ID-TAKEN',
InvalidKey = 'INVALID-KEY',
Leave = 'LEAVE',
Expire = 'EXPIRE',
}
/**
* An abstraction on top of WebSockets to provide fastest
* possible connection for peers.
*/
class Socket extends EventEmitter {
constructor(
secure: any,
host: string,
port: number,
path: string,
key: string,
pingInterval?: number
);
start(id: string, token: string): void;
/** Exposed send for DC & Peer. */
send(data: any): void;
close(): void;
}
class ServerMessage {
type: ServerMessageType;
payload: any;
src: string;
}
type BaseConnectionEvents = {
/**
* Emitted when either you or the remote peer closes the connection.
*/
close: () => void;
error: (error: Error) => void;
iceStateChanged: (state: RTCIceConnectionState) => void;
};
abstract class BaseConnection<
T extends EventEmitter.ValidEventTypes,
TT
> extends EventEmitter<T & BaseConnectionEvents> {
readonly peer: string;
provider: Peer<TT>;
readonly options: any;
protected _open: boolean;
readonly metadata: any;
connectionId: string;
peerConnection: RTCPeerConnection;
abstract get type(): ConnectionType;
get open(): boolean;
constructor(peer: string, provider: Peer<TT>, options: any);
abstract close(): void;
abstract handleMessage(message: ServerMessage): void;
}
type DataConnectionEvents<T> = {
/**
* Emitted when data is received from the remote peer.
*/
data: (data: T) => void;
/**
* Emitted when the connection is established and ready-to-use.
*/
open: () => void;
};
/**
* Wraps a DataChannel between two Peers.
*/
export class DataConnection<T> extends BaseConnection<
DataConnectionEvents<T>,
T
> {
readonly label: string;
readonly serialization: SerializationType;
readonly reliable: boolean;
stringify: (data: any) => string;
parse: (data: string) => any;
get type(): ConnectionType;
get dataChannel(): RTCDataChannel;
get bufferSize(): number;
constructor(peerId: string, provider: Peer<T>, options: any);
/** Called by the Negotiator when the DataChannel is ready. */
initialize(dc: RTCDataChannel): void;
/**
* Exposed functionality for users.
*/
/** Allows user to close connection. */
close(): void;
/** Allows user to send data. */
send(data: T, chunked?: boolean): void;
handleMessage(message: ServerMessage): void;
}
export interface AnswerOption {
sdpTransform?: Function;
}
export interface PeerJSOption {
key?: string;
host?: string;
port?: number;
path?: string;
secure?: boolean;
token?: string;
config?: RTCConfiguration;
debug?: number;
referrerPolicy?: ReferrerPolicy;
}
export interface PeerConnectOption {
label?: string;
metadata?: any;
serialization?: string;
reliable?: boolean;
}
export interface CallOption {
metadata?: any;
sdpTransform?: Function;
}
type MediaConnectionEvents = {
/**
* Emitted when a connection to the PeerServer is established.
*/
stream: (stream: MediaStream) => void;
};
/**
* Wraps the streaming interface between two Peers.
*/
export class MediaConnection<T> extends BaseConnection<
MediaConnectionEvents,
T
> {
get type(): ConnectionType;
get localStream(): MediaStream;
get remoteStream(): MediaStream;
constructor(peerId: string, provider: Peer<T>, options: any);
addStream(remoteStream: any): void;
handleMessage(message: ServerMessage): void;
answer(stream?: MediaStream, options?: AnswerOption): void;
/**
* Exposed functionality for users.
*/
/** Allows user to close connection. */
close(): void;
}
class PeerOptions implements PeerJSOption {
debug?: LogLevel;
host?: string;
port?: number;
path?: string;
key?: string;
token?: string;
config?: any;
secure?: boolean;
pingInterval?: number;
referrerPolicy?: ReferrerPolicy;
logFunction?: (logLevel: LogLevel, ...rest: any[]) => void;
}
type PeerEvents<T> = {
/**
* Emitted when a connection to the PeerServer is established.
*/
open: (id: string) => void;
/**
* Emitted when a new data connection is established from a remote peer.
*/
connection: (dataConnection: DataConnection<T>) => void;
/**
* Emitted when a remote peer attempts to call you.
*/
call: (mediaConnection: MediaConnection<T>) => void;
/**
* Emitted when the peer is destroyed and can no longer accept or create any new connections.
*/
close: () => void;
/**
* Emitted when the peer is disconnected from the signalling server
*/
disconnected: (currentId: string) => void;
/**
* Errors on the peer are almost always fatal and will destroy the peer.
*/
error: (error: Error) => void;
};
}
/**
* A peer who can initiate connections with other peers.
*/
declare class Peer<T> extends EventEmitter<Peer.PeerEvents<T>> {
/**
* The brokering ID of this peer
*/
get id(): string;
get options(): Peer.PeerOptions;
get open(): boolean;
get socket(): Peer.Socket;
/**
* A hash of all connections associated with this peer, keyed by the remote peer's ID.
* @deprecated
* Return type will change from Object to Map<string,[]>
*/
get connections(): Object;
/**
* true if this peer and all of its connections can no longer be used.
*/
get destroyed(): boolean;
/**
* false if there is an active connection to the PeerServer.
*/
get disconnected(): boolean;
/**
* A peer can connect to other peers and listen for connections.
*/
constructor();
/**
* A peer can connect to other peers and listen for connections.
* @param options for specifying details about PeerServer
*/
constructor(options: Peer.PeerOptions);
/**
* A peer can connect to other peers and listen for connections.
* @param id Other peers can connect to this peer using the provided ID.
* If no ID is given, one will be generated by the brokering server.
* @param options for specifying details about PeerServer
*/
constructor(id: string, options?: Peer.PeerOptions);
/** Retrieve messages from lost message store */
_getMessages(connectionId: string): Peer.ServerMessage[];
/**
* Connects to the remote peer specified by id and returns a data connection.
* @param peer The brokering ID of the remote peer (their peer.id).
* @param options for specifying details about Peer Connection
*/
connect(
peer: string,
options?: Peer.PeerConnectOption
): Peer.DataConnection<T>;
/**
* Calls the remote peer specified by id and returns a media connection.
* @param peer The brokering ID of the remote peer (their peer.id).
* @param stream The caller's media stream
* @param options Metadata associated with the connection, passed in by whoever initiated the connection.
*/
call(
peer: string,
stream: MediaStream,
options?: Peer.CallOption
): Peer.MediaConnection<T>;
_removeConnection(
connection: Peer.DataConnection<T> | Peer.MediaConnection<T>
): void;
/** Retrieve a data/media connection for this peer. */
getConnection(
peerId: string,
connectionId: string
): null | Peer.DataConnection<T> | Peer.MediaConnection<T>;
/** Emits a typed error message. */
emitError(type: Peer.PeerErrorType, err: string | Error): void;
/**
* Destroys the Peer: closes all active connections as well as the connection
* to the server.
* Warning: The peer can no longer create or accept connections after being
* destroyed.
*/
destroy(): void;
/**
* Disconnects the Peer's connection to the PeerServer. Does not close any
* active connections.
* Warning: The peer can no longer create or accept connections after being
* disconnected. It also cannot reconnect to the server.
*/
disconnect(): void;
/** Attempts to reconnect with the same ID. */
reconnect(): void;
/**
* Get a list of available peer IDs. If you're running your own server, you'll
* want to set allow_discovery: true in the PeerServer options. If you're using
* the cloud server, email team@peerjs.com to get the functionality enabled for
* your key.
*/
listAllPeers(cb?: (_: any[]) => void): void;
}

View File

@@ -91,12 +91,12 @@ describe('Multiplayer', () => {
};
/**
* A mocked P2P event data.
* @implements {gdjs.evtTools.p2p.IEventData}
* A mocked P2P message data.
* @implements {gdjs.multiplayerPeerJsHelper.IMessageData}
*/
class MockedEventData {
class MockedMessageData {
/**
* @param {string} data
* @param {object} data
* @param {string} sender
**/
constructor(data, sender) {
@@ -107,59 +107,63 @@ describe('Multiplayer', () => {
/**
* The data sent alongside the event.
*/
data = '';
data = {};
/**
* The ID of the sender of the event.
*/
sender = '';
}
/**
* A mocked P2P event.
* @implements {gdjs.evtTools.p2p.IEvent}
*/
class MockedEvent {
data = [];
dataloss = false;
isTriggered() {
return this.data.length > 0;
}
/**
* @param {gdjs.evtTools.p2p.IEventData} newData
*/
pushData(newData) {
if (this.dataloss && this.data.length > 0) this.data[0] = newData;
else this.data.push(newData);
}
popData() {
this.data.shift();
}
getData() {
return this.data.length === 0 ? '' : this.data[0].data;
return this.data;
}
getSender() {
return this.data.length === 0 ? '' : this.data[0].sender;
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 events sent to/from peers.
* It stores the messages sent to/from peers.
*/
const createP2PAndMultiplayerManagersMock = () => {
const createMultiplayerManagersMock = () => {
const p2pState = {
currentPeerId: '',
otherPeerIds: [],
};
/** @type {Record<string, Map<string, MockedEvent>>} */
const peerEvents = {};
/** @type {Record<string, Map<string, MockedMessagesList>>} */
const peerAllMessagesMap = {};
/** @type {Record<string, gdjs.MultiplayerMessageManager>} */
const peerMultiplayerMessageManager = {};
@@ -167,88 +171,68 @@ describe('Multiplayer', () => {
/** @type {Record<string, gdjs.MultiplayerVariablesManager>} */
const peerMultiplayerVariablesManager = {};
const getPeerEvents = (peerId) =>
(peerEvents[peerId] = peerEvents[peerId] || new Map());
const getPeerMessages = (peerId) =>
(peerAllMessagesMap[peerId] = peerAllMessagesMap[peerId] || new Map());
/**
* @param {string} eventName
* @returns {gdjs.evtTools.p2p.IEvent}
* @param {string} messageName
* @returns {gdjs.multiplayerPeerJsHelper.IMessagesList}
*/
const getEvent = (eventName) => {
const events = getPeerEvents(p2pState.currentPeerId);
let event = events.get(eventName);
if (!event) events.set(eventName, (event = new MockedEvent()));
return event;
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} peerId
* @param {string} eventName
* @param {string} eventData
* @param {string[]} peerIds
* @param {string} messageName
* @param {object} messageData
*/
const sendDataTo = (peerId, eventName, eventData) => {
// console.log(`## SENDING DATA TO ${peerId}:`, eventName, eventData);
const events = getPeerEvents(peerId);
let event = events.get(eventName);
if (!event) events.set(eventName, (event = new MockedEvent()));
event.pushData(new MockedEventData(eventData, peerId));
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);
}
peerMessagesList.pushMessage(messageData, p2pState.currentPeerId);
}
};
/** @type {typeof gdjs.evtTools.p2p} */
const p2pMock = {
/** @type {typeof gdjs.multiplayerPeerJsHelper} */
const peerJsHelperMock = {
// @ts-ignore - this is a mock so private properties can't be the same.
Event: MockedEvent,
EventData: MockedEventData,
sendVariableTo: () => {},
sendVariableToAll: () => {},
getEventVariable: (eventName, variable) => {
variable.fromJSON(getEvent(eventName).getData());
},
onEvent: (eventName, dataloss) => {
const event = getEvent(eventName);
event.dataloss = dataloss;
const isTriggered = event.isTriggered();
return isTriggered;
},
getEvent,
MessagesList: MockedMessagesList,
MessageData: MockedMessageData,
getOrCreateMessagesList,
connect: (id) => {},
disconnectFromPeer: (id) => {},
disconnectFromAllPeers: () => {},
disconnectFromAll: () => {},
disconnectFromBroker: () => {},
sendDataTo,
sendDataToAll: (eventName, eventData) => {
p2pState.otherPeerIds.forEach((peerId) => {
sendDataTo(peerId, eventName, eventData);
});
},
getEventData: (eventName) => getEvent(eventName).getData(),
getEventSender: (eventName) => getEvent(eventName).getSender(),
getEvents: () => getPeerEvents(p2pState.currentPeerId),
getAllMessagesMap: () => getPeerMessages(p2pState.currentPeerId),
useCustomBrokerServer: () => {},
useDefaultBrokerServer: () => {},
useCustomICECandidate: () => {},
forceUseRelayServer: (shouldUseRelayServer) => {},
overrideId: (id) => {},
getCurrentId: () => 'fake-current-id',
isReady: () => true,
onError: () => false,
getLastError: () => '',
onDisconnect: () => false,
getDisconnectedPeer: () => '',
onConnection: () => false,
getConnectedPeer: () => '',
getJustDisconnectedPeers: () => [],
getAllPeers: () => p2pState.otherPeerIds,
getConnectionInstance: () => undefined,
};
gdjs.evtTools.p2p = p2pMock;
gdjs.multiplayerPeerJsHelper = peerJsHelperMock;
return {
switchToPeer: ({ peerId, otherPeerIds, playerNumber }) => {
// console.log('## SWITCHING TO PEER', peerId);
// Switch the state of the P2P mock.
// Switch the state of the peerJs mock.
p2pState.currentPeerId = peerId;
p2pState.otherPeerIds = otherPeerIds;
@@ -273,25 +257,29 @@ describe('Multiplayer', () => {
// Switch the state of the game.
gdjs.multiplayer.playerNumber = playerNumber;
},
logEvents: () => {
Object.keys(peerEvents).forEach((peerId) => {
console.log(`## PEER ${peerId} events:`);
for (const [eventName, event] of peerEvents[peerId]) {
console.log(`${eventName}: ${JSON.stringify(event.data)}`);
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())}`
);
}
});
},
markAllPeerEventsAsProcessed: () => {
for (const events of Object.values(peerEvents)) {
for (const event of events.values()) {
event.popData();
markAllPeerMessagesAsProcessed: () => {
for (const allMessagesList of Object.values(peerAllMessagesMap)) {
for (const messagesList of allMessagesList.values()) {
messagesList.data = [];
}
}
},
expectNoEventsToBeProcessed: () => {
for (const events of Object.values(peerEvents)) {
for (const event of events.values()) {
expect(event.isTriggered()).to.be(false);
expectNoMessagesToBeProcessed: () => {
for (const allMessagesList of Object.values(peerAllMessagesMap)) {
for (const messagesList of allMessagesList.values()) {
expect(messagesList.getMessages().length).to.be(0);
}
}
},
@@ -337,8 +325,8 @@ describe('Multiplayer', () => {
it('synchronizes scene/global variables from the host to other players', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerManagersMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
switchToPeer({
peerId: 'player-1',
@@ -370,7 +358,7 @@ describe('Multiplayer', () => {
const p2RuntimeScene = makeTestRuntimeSceneWithNetworkId();
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
expect(p2RuntimeScene.getVariables().has('MyString_Variable')).to.be(
true
);
@@ -459,7 +447,7 @@ describe('Multiplayer', () => {
});
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
expect(
p2RuntimeScene.getGame().getVariables().has('MyGlobalStringVariable')
).to.be(true);
@@ -529,9 +517,9 @@ describe('Multiplayer', () => {
it('overrides a scene/global variable, modified by a player, when synchronized by the host', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
expectNoEventsToBeProcessed,
} = createP2PAndMultiplayerManagersMock();
markAllPeerMessagesAsProcessed,
expectNoMessagesToBeProcessed,
} = createMultiplayerManagersMock();
switchToPeer({
peerId: 'player-1',
@@ -566,7 +554,7 @@ describe('Multiplayer', () => {
p2RuntimeScene.getVariables().add('MyOtherVariable', variable);
}
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
expect(
p2RuntimeScene.getVariables().get('MyVariable').getAsString()
).to.be('Hello from remote world');
@@ -574,7 +562,7 @@ describe('Multiplayer', () => {
p2RuntimeScene.getVariables().get('MyOtherVariable').getAsString()
).to.be('Something else');
expectNoEventsToBeProcessed();
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).
@@ -605,16 +593,16 @@ describe('Multiplayer', () => {
p2RuntimeScene.getVariables().get('MyOtherVariable').getAsString()
).to.be('Something else');
markAllPeerEventsAsProcessed();
expectNoEventsToBeProcessed();
markAllPeerMessagesAsProcessed();
expectNoMessagesToBeProcessed();
});
it('synchronizes a scene/global variable from a player to the host to other players', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
expectNoEventsToBeProcessed,
} = createP2PAndMultiplayerManagersMock();
markAllPeerMessagesAsProcessed,
expectNoMessagesToBeProcessed,
} = createMultiplayerManagersMock();
switchToPeer({
peerId: 'player-1',
@@ -678,8 +666,8 @@ describe('Multiplayer', () => {
p3GlobalVariable.setPlayerOwnership(3); // Ownership is given to player 3.
p3RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
expectNoEventsToBeProcessed();
markAllPeerMessagesAsProcessed();
expectNoMessagesToBeProcessed();
// Change the variables on player 3.
{
@@ -747,9 +735,9 @@ describe('Multiplayer', () => {
it('does not synchronize a scene/global variable from players if defined as not synchronized', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
expectNoEventsToBeProcessed,
} = createP2PAndMultiplayerManagersMock();
markAllPeerMessagesAsProcessed,
expectNoMessagesToBeProcessed,
} = createMultiplayerManagersMock();
switchToPeer({
peerId: 'player-1',
@@ -813,8 +801,8 @@ describe('Multiplayer', () => {
p3GlobalVariable.disableSynchronization(); // Disable synchronization.
p3RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
expectNoEventsToBeProcessed();
markAllPeerMessagesAsProcessed();
expectNoMessagesToBeProcessed();
// Change the variables on player 3.
{
@@ -945,8 +933,8 @@ describe('Multiplayer', () => {
it('synchronizes objects from the host to other players', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerManagersMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
// Create an instance on the host's game:
switchToPeer({
@@ -980,7 +968,7 @@ describe('Multiplayer', () => {
if (!p2Objects) throw new Error('No objects found');
expect(p2Objects.length).to.be(0);
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
const {
object: p2SpriteObject,
@@ -1021,7 +1009,7 @@ describe('Multiplayer', () => {
playerNumber: 2,
});
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
const {
object: p2SpriteObject,
@@ -1061,7 +1049,7 @@ describe('Multiplayer', () => {
playerNumber: 2,
});
p2RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
const p2Objects = p2RuntimeScene.getObjects('MySpriteObject');
if (!p2Objects) throw new Error('No objects found');
@@ -1073,8 +1061,8 @@ describe('Multiplayer', () => {
it('synchronizes objects from a player to the host to other players', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerManagersMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
// Create an instance on a player:
switchToPeer({
@@ -1144,7 +1132,7 @@ describe('Multiplayer', () => {
expect(p3SpriteObject.getX()).to.be(142);
expect(p3SpriteObject.getY()).to.be(143);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
// Move the object on the player:
{
@@ -1196,7 +1184,7 @@ describe('Multiplayer', () => {
playerNumber: 3,
});
p3RuntimeScene.renderAndStep(1000 / 60);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
const {
object: p3SpriteObject,
@@ -1270,15 +1258,15 @@ describe('Multiplayer', () => {
expect(p3ObjectsAndBehaviorsUpdated.length).to.be(0);
}
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
});
it('allows ownership to change from host to a player to another player', () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
expectNoEventsToBeProcessed,
} = createP2PAndMultiplayerManagersMock();
markAllPeerMessagesAsProcessed,
expectNoMessagesToBeProcessed,
} = createMultiplayerManagersMock();
// Create an instance on the host's game:
switchToPeer({
@@ -1335,8 +1323,8 @@ describe('Multiplayer', () => {
expect(p3SpriteObjectOriginal.getX()).to.be(142);
expect(p3SpriteObjectOriginal.getY()).to.be(143);
markAllPeerEventsAsProcessed();
expectNoEventsToBeProcessed();
markAllPeerMessagesAsProcessed();
expectNoMessagesToBeProcessed();
// Check player 3 can get ownership (and can directly move the instance, without waiting for the
// host to acknowledge the change).
@@ -1406,9 +1394,9 @@ describe('Multiplayer', () => {
expect(p2SpriteObject.getX()).to.be(342);
expect(p2SpriteObject.getY()).to.be(343);
markAllPeerEventsAsProcessed();
markAllPeerEventsAsProcessed();
expectNoEventsToBeProcessed();
markAllPeerMessagesAsProcessed();
markAllPeerMessagesAsProcessed();
expectNoMessagesToBeProcessed();
}
// Check player 2 can get ownership.
@@ -1492,7 +1480,7 @@ describe('Multiplayer', () => {
).to.be(2);
}
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
// Check that the position given by player 2 is updated on the host and player 3.
{
@@ -1545,15 +1533,15 @@ describe('Multiplayer', () => {
expect(p3SpriteObject.getY()).to.be(243);
}
markAllPeerEventsAsProcessed();
expectNoEventsToBeProcessed();
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,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerManagersMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
// Create an instance on a player:
switchToPeer({
@@ -1625,7 +1613,7 @@ describe('Multiplayer', () => {
expect(p3SpriteObject.getX()).to.be(142);
expect(p3SpriteObject.getY()).to.be(143);
markAllPeerEventsAsProcessed();
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".
@@ -1756,12 +1744,12 @@ describe('Multiplayer', () => {
expect(p3Object2.getX()).to.be(42);
expect(p3Object2.getY()).to.be(43);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
}
});
it('deletes an instance owned by another player after a bit (if not "reconciled" in the meantime)', async () => {
const { switchToPeer } = createP2PAndMultiplayerManagersMock();
const { switchToPeer } = createMultiplayerManagersMock();
// 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
@@ -1813,8 +1801,8 @@ describe('Multiplayer', () => {
it('gives priority to the first ownership change and revert the wrong one', async () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerManagersMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
// Create an instance on the host's game:
switchToPeer({
@@ -1887,7 +1875,7 @@ describe('Multiplayer', () => {
expect(p3SpriteObject.getX()).to.be(142);
expect(p3SpriteObject.getY()).to.be(143);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
// Now, try to change ownership to player 2 and 3 at the "same time".
{
@@ -1953,7 +1941,7 @@ describe('Multiplayer', () => {
p1SpriteMultiplayerObjectBehaviorUpdated.getPlayerObjectOwnership()
).to.be(2);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
}
// Wait so that player 3 retries.
@@ -1980,7 +1968,7 @@ describe('Multiplayer', () => {
p3SpriteMultiplayerObjectBehavior.getPlayerObjectOwnership()
).to.be(3);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
await delay(210);
}
@@ -2005,7 +1993,7 @@ describe('Multiplayer', () => {
expect(
p3SpriteMultiplayerObjectBehavior.getPlayerObjectOwnership()
).to.be(0);
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
}
// Move the object on the player 2:
@@ -2072,7 +2060,7 @@ describe('Multiplayer', () => {
p3SpriteMultiplayerObjectBehavior.getPlayerObjectOwnership()
).to.be(2);
}
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
});
});
@@ -2091,7 +2079,7 @@ describe('Multiplayer', () => {
};
it('synchronizes scenes from the host to other players', async () => {
const { switchToPeer } = createP2PAndMultiplayerManagersMock();
const { switchToPeer } = createMultiplayerManagersMock();
const gameLayoutData = [
getFakeSceneAndExtensionData({ name: 'Scene1' }).sceneData,
@@ -2218,8 +2206,8 @@ describe('Multiplayer', () => {
it('reconciles a scene launched both by the host and by a player', async () => {
const {
switchToPeer,
markAllPeerEventsAsProcessed,
} = createP2PAndMultiplayerManagersMock();
markAllPeerMessagesAsProcessed,
} = createMultiplayerManagersMock();
const gameLayoutData = [
getFakeSceneAndExtensionData({ name: 'Scene1' }).sceneData,
@@ -2263,7 +2251,7 @@ describe('Multiplayer', () => {
p2RuntimeGame.getSceneStack().step(1000 / 60);
checkCurrentSceneIs(p2RuntimeGame, 'Scene1');
markAllPeerEventsAsProcessed();
markAllPeerMessagesAsProcessed();
// Launch a second scene, first on the player:
p2RuntimeGame.getSceneStack().push('Scene2');