gdcore-tools 2.0.0-gd-v5.4.217-autobuild → 2.0.0-gd-v5.4.219-autobuild
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Runtime/Extensions/3D/CustomRuntimeObject3D.js +1 -1
- package/dist/Runtime/Extensions/3D/CustomRuntimeObject3D.js.map +2 -2
- package/dist/Runtime/Extensions/3D/JsExtension.js +7 -3
- package/dist/Runtime/Extensions/AnchorBehavior/anchorruntimebehavior.js +1 -1
- package/dist/Runtime/Extensions/AnchorBehavior/anchorruntimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/DialogueTree/JsExtension.js +44 -32
- package/dist/Runtime/Extensions/DialogueTree/dialoguetools.js +2 -2
- package/dist/Runtime/Extensions/DialogueTree/dialoguetools.js.map +2 -2
- package/dist/Runtime/Extensions/Multiplayer/JsExtension.js +15 -0
- package/dist/Runtime/Extensions/Multiplayer/messageManager.js +1 -1
- package/dist/Runtime/Extensions/Multiplayer/messageManager.js.map +2 -2
- package/dist/Runtime/Extensions/Multiplayer/multiplayerobjectruntimebehavior.js +1 -1
- package/dist/Runtime/Extensions/Multiplayer/multiplayerobjectruntimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/Physics2Behavior/physics2runtimebehavior.js +1 -1
- package/dist/Runtime/Extensions/Physics2Behavior/physics2runtimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/Spine/managers/pixi-spine-atlas-manager.js +1 -1
- package/dist/Runtime/Extensions/Spine/managers/pixi-spine-atlas-manager.js.map +2 -2
- package/dist/Runtime/Extensions/Spine/managers/pixi-spine-manager.js +1 -1
- package/dist/Runtime/Extensions/Spine/managers/pixi-spine-manager.js.map +2 -2
- package/dist/Runtime/Extensions/TileMap/simpletilemapruntimeobject.js +1 -1
- package/dist/Runtime/Extensions/TileMap/simpletilemapruntimeobject.js.map +2 -2
- package/dist/Runtime/Model3DManager.js +1 -1
- package/dist/Runtime/Model3DManager.js.map +2 -2
- package/dist/Runtime/ResourceLoader.js +1 -1
- package/dist/Runtime/ResourceLoader.js.map +2 -2
- package/dist/Runtime/RuntimeLayer.js +1 -1
- package/dist/Runtime/RuntimeLayer.js.map +2 -2
- package/dist/Runtime/capturemanager.js +2 -0
- package/dist/Runtime/capturemanager.js.map +7 -0
- package/dist/Runtime/fontfaceobserver-font-manager/fontfaceobserver-font-manager.js +1 -1
- package/dist/Runtime/fontfaceobserver-font-manager/fontfaceobserver-font-manager.js.map +2 -2
- package/dist/Runtime/howler-sound-manager/howler-sound-manager.js +1 -1
- package/dist/Runtime/howler-sound-manager/howler-sound-manager.js.map +2 -2
- package/dist/Runtime/jsonmanager.js +1 -1
- package/dist/Runtime/jsonmanager.js.map +2 -2
- package/dist/Runtime/layer.js +1 -1
- package/dist/Runtime/layer.js.map +2 -2
- package/dist/Runtime/pixi-renderers/pixi-bitmapfont-manager.js +1 -1
- package/dist/Runtime/pixi-renderers/pixi-bitmapfont-manager.js.map +2 -2
- package/dist/Runtime/pixi-renderers/pixi-image-manager.js +1 -1
- package/dist/Runtime/pixi-renderers/pixi-image-manager.js.map +2 -2
- package/dist/Runtime/pixi-renderers/runtimegame-pixi-renderer.js +1 -1
- package/dist/Runtime/pixi-renderers/runtimegame-pixi-renderer.js.map +2 -2
- package/dist/Runtime/runtimegame.js +1 -1
- package/dist/Runtime/runtimegame.js.map +2 -2
- package/dist/Runtime/runtimewatermark.js +2 -2
- package/dist/Runtime/runtimewatermark.js.map +2 -2
- package/dist/Runtime/scenestack.js +1 -1
- package/dist/Runtime/scenestack.js.map +2 -2
- package/dist/Runtime/spriteruntimeobject.js +1 -1
- package/dist/Runtime/spriteruntimeobject.js.map +2 -2
- package/dist/Runtime/types/global-types.d.ts +20 -3
- package/dist/Runtime/types/project-data.d.ts +1 -1
- package/dist/lib/libGD.cjs +1 -1
- package/dist/lib/libGD.wasm +0 -0
- package/gd.d.ts +35 -7
- package/package.json +1 -1
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../GDevelop/Extensions/Multiplayer/messageManager.ts"],
|
|
4
|
-
"sourcesContent": ["namespace gdjs {\n const logger = new gdjs.Logger('Multiplayer');\n const debugLogger = new gdjs.Logger('Multiplayer - Debug');\n // Comment this to see message logs and ease debugging:\n gdjs.Logger.getDefaultConsoleLoggerOutput().discardGroup(\n 'Multiplayer - Debug'\n );\n\n class RecentlySeenKeys {\n maxSize: number;\n cache: Set<string>;\n keys: string[];\n\n constructor(maxSize: number) {\n this.maxSize = maxSize;\n this.cache = new Set();\n this.keys = [];\n }\n\n has(key: string) {\n return this.cache.has(key);\n }\n\n add(key: string) {\n // If we are at the maximum size, remove the first key.\n if (this.cache.size >= this.maxSize) {\n const keyToRemove = this.keys.shift();\n if (keyToRemove) {\n this.cache.delete(keyToRemove);\n }\n }\n\n // Add the key to the end of the list.\n this.cache.add(key);\n this.keys.push(key);\n }\n\n clear = () => {\n this.cache.clear();\n this.keys = [];\n };\n }\n\n class SavedSyncDataUpdates<T> {\n private _updates: T[] = [];\n\n store(update: T) {\n this._updates.push(update);\n if (this._updates.length > 10) {\n this._updates.shift();\n }\n }\n\n getUpdates() {\n return this._updates;\n }\n\n remove(update: T) {\n const index = this._updates.indexOf(update);\n if (index !== -1) {\n this._updates.splice(index, 1);\n }\n }\n\n clear() {\n this._updates = [];\n }\n }\n\n /**\n * Helper function to clone an object without reassigning the target object.\n * It's mainly helpful for tests, where multiple instances of the MultiplayerMessageManager are created,\n * and prevents keeping references to the same object.\n */\n const cloneObjectWithoutOverwriting = ({\n target,\n source,\n }: {\n target: Object;\n source: Object;\n }) => {\n // Add the new properties.\n for (const key in source) {\n if (source.hasOwnProperty(key) && !target.hasOwnProperty(key)) {\n target[key] = source[key];\n }\n }\n\n // Remove the properties that are not in the source.\n for (const key in target) {\n if (target.hasOwnProperty(key) && !source.hasOwnProperty(key)) {\n delete target[key];\n }\n }\n };\n\n export type MultiplayerMessageManager = ReturnType<\n typeof makeMultiplayerMessageManager\n >;\n\n /**\n * Create a new MultiplayerMessageManager.\n *\n * In most cases, you should use the default `gdjs.multiplayerMessageManager` instead.\n *\n * @returns\n */\n export const makeMultiplayerMessageManager = () => {\n // For testing purposes, you can simulate network latency and packet loss.\n // Adds x ms to all network messages, simulating a slow network.\n const SIMULATE_NETWORK_LATENCY_MS = 0; // In ms.\n // Gives a random chance of packet loss, simulating a bad network.\n const SIMULATE_NETWORK_PACKET_LOSS_CHANCE = 0; // Between 0 and 1, % of packets lost.\n // Adds a latency to random network messages, simulating sporadic network issues.\n const SIMULATE_NETWORK_RANDOM_SLOW_PACKET_CHANCE = 0; // Between 0 and 1, % of packets that will be slow.\n const SIMULATE_NETWORK_RANDOM_LATENCY_MS = 0; // In ms.\n\n const getTimeNow =\n window.performance && typeof window.performance.now === 'function'\n ? window.performance.now.bind(window.performance)\n : Date.now;\n const defaultMessageRetryTime = 200; // Time to wait before retrying a message that was not acknowledged, in ms.\n const defaultMaxRetries = 4; // Maximum number of retries before giving up on a message.\n\n // Make the processed messages an LRU cache, so that we can limit the number of messages we keep in memory,\n // as well as keep them in order.\n const processedCustomMessagesCache = new RecentlySeenKeys(500);\n\n let expectedMessageAcknowledgements: {\n [messageName: string]: {\n [peerId: string]: {\n acknowledged: boolean;\n lastMessageSentAt: number;\n originalMessageName: string;\n originalData: any;\n numberOfRetries: number;\n maxNumberOfRetries: number;\n messageRetryTime: number;\n shouldCancelMessageIfTimesOut?: boolean;\n };\n };\n } = {};\n let _lastClockReceivedByInstanceByScene: {\n [sceneId: string]: { [instanceId: string]: number };\n } = {};\n\n // The number of times per second the scene data should be synchronized.\n const sceneSyncDataSyncRate = 1;\n let lastSceneSyncTimestamp = 0;\n let lastSentSceneSyncData: LayoutNetworkSyncData | null = null;\n let numberOfForcedSceneUpdates = 0;\n let lastReceivedSceneSyncDataUpdates = new SavedSyncDataUpdates<\n LayoutNetworkSyncData\n >();\n\n // The number of times per second the game data should be synchronized.\n const gameSyncDataSyncRate = 1;\n let lastGameSyncTimestamp = 0;\n let lastSentGameSyncData: GameNetworkSyncData | null = null;\n let numberOfForcedGameUpdates = 0;\n let lastReceivedGameSyncDataUpdates = new SavedSyncDataUpdates<\n GameNetworkSyncData\n >();\n\n // Send heartbeat messages from host to players, ensuring their connection is still alive,\n // measure the ping, and send other useful info.\n const heartbeatSyncRate = 1;\n let lastHeartbeatSentTimestamp = 0;\n let _playersLastRoundTripTimes: {\n [playerNumber: number]: number[];\n } = {};\n let _peerIdToPlayerNumber: { [peerId: string]: number } = {};\n let _playersInfo: {\n [playerNumber: number]: {\n ping: number;\n playerId: string;\n username: string;\n };\n } = {};\n let _playerNumbersWhoJustLeft: number[] = [];\n let _playerNumbersWhoJustJoined: number[] = [];\n let _temporaryPlayerNumberToUsername: {\n [playerNumber: number]: string;\n } = {};\n\n const addExpectedMessageAcknowledgement = ({\n originalMessageName,\n originalData,\n expectedMessageName,\n otherPeerIds,\n shouldCancelMessageIfTimesOut,\n maxNumberOfRetries,\n messageRetryTime,\n }: {\n originalMessageName: string;\n originalData: any;\n expectedMessageName: string;\n otherPeerIds: string[];\n shouldCancelMessageIfTimesOut: boolean;\n maxNumberOfRetries?: number;\n messageRetryTime?: number;\n }) => {\n if (!gdjs.multiplayer.isLobbyGameRunning()) {\n // This can happen if objects are destroyed at the end of the scene.\n // We should not add expected messages in this case.\n return;\n }\n\n if (!expectedMessageAcknowledgements[expectedMessageName]) {\n expectedMessageAcknowledgements[expectedMessageName] = {};\n }\n\n debugLogger.info(\n `Adding expected message ${expectedMessageName} from ${otherPeerIds.join(\n ', '\n )}.`\n );\n\n otherPeerIds.forEach((peerId) => {\n expectedMessageAcknowledgements[expectedMessageName][peerId] = {\n acknowledged: false,\n lastMessageSentAt: getTimeNow(),\n originalMessageName,\n originalData,\n shouldCancelMessageIfTimesOut,\n numberOfRetries: 0,\n maxNumberOfRetries: maxNumberOfRetries || defaultMaxRetries,\n messageRetryTime: messageRetryTime || defaultMessageRetryTime,\n };\n });\n };\n\n const getLastClockReceivedForInstanceOnScene = ({\n sceneNetworkId,\n instanceNetworkId,\n }: {\n sceneNetworkId: string;\n instanceNetworkId: string;\n }) => {\n if (!_lastClockReceivedByInstanceByScene[sceneNetworkId]) {\n _lastClockReceivedByInstanceByScene[sceneNetworkId] = {};\n }\n\n return (\n _lastClockReceivedByInstanceByScene[sceneNetworkId][\n instanceNetworkId\n ] || 0\n );\n };\n\n const setLastClockReceivedForInstanceOnScene = ({\n sceneNetworkId,\n instanceNetworkId,\n clock,\n }: {\n sceneNetworkId: string;\n instanceNetworkId: string;\n clock: number;\n }) => {\n if (!_lastClockReceivedByInstanceByScene[sceneNetworkId]) {\n _lastClockReceivedByInstanceByScene[sceneNetworkId] = {};\n }\n\n _lastClockReceivedByInstanceByScene[sceneNetworkId][\n instanceNetworkId\n ] = clock;\n };\n\n /**\n * Main function to send messages to other players, via P2P.\n * Takes into account the simulation of network latency and packet loss.\n */\n const sendDataTo = (\n peerIds: string[],\n messageName: string,\n data: object\n ): void => {\n if (\n SIMULATE_NETWORK_PACKET_LOSS_CHANCE > 0 &&\n Math.random() < SIMULATE_NETWORK_PACKET_LOSS_CHANCE\n ) {\n return;\n }\n\n if (\n SIMULATE_NETWORK_RANDOM_SLOW_PACKET_CHANCE > 0 &&\n Math.random() < SIMULATE_NETWORK_RANDOM_SLOW_PACKET_CHANCE\n ) {\n setTimeout(() => {\n gdjs.multiplayerPeerJsHelper.sendDataTo(peerIds, messageName, data);\n }, SIMULATE_NETWORK_RANDOM_LATENCY_MS);\n return;\n }\n\n if (SIMULATE_NETWORK_LATENCY_MS > 0) {\n setTimeout(() => {\n gdjs.multiplayerPeerJsHelper.sendDataTo(peerIds, messageName, data);\n }, SIMULATE_NETWORK_LATENCY_MS);\n return;\n }\n\n gdjs.multiplayerPeerJsHelper.sendDataTo(peerIds, messageName, data);\n };\n\n const findClosestInstanceWithoutNetworkId = (\n instances: gdjs.RuntimeObject[],\n x: number,\n y: number\n ): gdjs.RuntimeObject | null => {\n if (!instances.length) {\n // No instances, return null.\n return null;\n }\n\n // Avoid using a reduce function to avoid creating a new object at each iteration.\n let closestInstance: gdjs.RuntimeObject | null = null;\n let closestDistance = Infinity;\n for (let i = 0; i < instances.length; ++i) {\n if (instances[i].networkId) {\n // Skip instances that already have a network ID.\n continue;\n }\n\n const instance = instances[i];\n const distance =\n Math.pow(instance.getX() - x, 2) + Math.pow(instance.getY() - y, 2);\n if (distance < closestDistance) {\n closestInstance = instance;\n closestDistance = distance;\n }\n }\n\n return closestInstance;\n };\n\n const getInstanceFromNetworkId = ({\n runtimeScene,\n objectName,\n instanceNetworkId,\n instanceX,\n instanceY,\n shouldCreateIfNotFound,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n objectName: string;\n instanceNetworkId: string;\n instanceX?: number;\n instanceY?: number;\n shouldCreateIfNotFound?: boolean;\n }): gdjs.RuntimeObject | null => {\n const instances = runtimeScene.getInstancesOf(objectName);\n if (!instances) {\n // object does not exist in the scene, cannot find the instance.\n return null;\n }\n let instance =\n instances.find(\n (instance) => instance.networkId === instanceNetworkId\n ) || null;\n\n // If we know the position of the object, we can try to find the closest instance not synchronized yet.\n if (!instance && instanceX !== undefined && instanceY !== undefined) {\n debugLogger.info(\n `instance ${objectName} ${instanceNetworkId} not found with network ID, trying to find it with position ${instanceX}/${instanceY}.`\n );\n // Instance not found, it must be a new object.\n // 2 cases :\n // - The object was only created on the other player's game, so we create it and assign it the network ID.\n // - The object may have been created on all sides at the same time, so we try to find instances\n // of this object, that do not have a network ID yet, pick the one that is the closest to the\n // position of the object created by the other player, and assign it the network ID to start\n // synchronizing it.\n\n // Try to assign the network ID to the instance that is the closest to the position of the object created by the other player.\n const closestInstance = findClosestInstanceWithoutNetworkId(\n instances,\n instanceX,\n instanceY\n );\n\n if (closestInstance) {\n debugLogger.info(\n `Found closest instance for object ${objectName} ${instanceNetworkId} with no network ID.`\n );\n\n instance = closestInstance;\n instance.networkId = instanceNetworkId;\n }\n }\n\n // If we still did not find the instance, and we should create it if not found, then create it.\n if (!instance && shouldCreateIfNotFound) {\n debugLogger.info(\n `Instance ${instanceNetworkId} still not found, Creating instance ${objectName}.`\n );\n const newInstance = runtimeScene.createObject(objectName);\n if (!newInstance) {\n // Object does not exist in the scene, cannot create the instance.\n return null;\n }\n\n newInstance.networkId = instanceNetworkId;\n instance = newInstance;\n }\n\n return instance;\n };\n\n const changeInstanceOwnerMessageNamePrefix = '#changeInstanceOwner';\n const changeInstanceOwnerMessageNameRegex = /#changeInstanceOwner#owner_(\\d+)#object_(.+)#instance_(.+)/;\n const createChangeInstanceOwnerMessage = ({\n objectOwner,\n objectName,\n instanceNetworkId,\n newObjectOwner,\n instanceX,\n instanceY,\n sceneNetworkId,\n }: {\n objectOwner: number;\n objectName: string;\n instanceNetworkId: string;\n newObjectOwner: number;\n instanceX: number;\n instanceY: number;\n sceneNetworkId: string;\n }): {\n messageName: string;\n messageData: {\n previousOwner: number;\n newOwner: number;\n instanceX: number;\n instanceY: number;\n sceneNetworkId: string;\n };\n } => {\n return {\n messageName: `${changeInstanceOwnerMessageNamePrefix}#owner_${objectOwner}#object_${objectName}#instance_${instanceNetworkId}`,\n messageData: {\n previousOwner: objectOwner,\n newOwner: newObjectOwner,\n instanceX,\n instanceY,\n sceneNetworkId,\n },\n };\n };\n const instanceOwnerChangedMessageNamePrefix = '#instanceOwnerChanged';\n const instanceOwnerChangedMessageNameRegex = /#instanceOwnerChanged#owner_(\\d+)#object_(.+)#instance_(.+)/;\n const createInstanceOwnerChangedMessageNameFromChangeInstanceOwnerMessage = (\n messageName: string\n ): string => {\n return messageName.replace(\n changeInstanceOwnerMessageNamePrefix,\n instanceOwnerChangedMessageNamePrefix\n );\n };\n const handleChangeInstanceOwnerMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Change owner messages do not need to be saved for later use, as the game will automatically change the owner of\n // the instance when receiving an update message with a different owner.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n\n // When we receive ownership change messages, update the ownership of the instances in the scene.\n const instanceOwnershipChangeMessageNames = messageNamesArray.filter(\n (messageName) =>\n messageName.startsWith(changeInstanceOwnerMessageNamePrefix)\n );\n instanceOwnershipChangeMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const matches = changeInstanceOwnerMessageNameRegex.exec(messageName);\n if (!matches) {\n return;\n }\n const objectName = matches[2];\n const instanceNetworkId = matches[3];\n const previousOwner = messageData.previousOwner;\n const newOwner = messageData.newOwner;\n const sceneNetworkId = messageData.sceneNetworkId;\n\n if (sceneNetworkId !== runtimeScene.networkId) {\n debugLogger.info(\n `Object ${objectName} is in scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n // The object is not in the current scene.\n return;\n }\n\n const instance = getInstanceFromNetworkId({\n runtimeScene,\n objectName,\n instanceNetworkId,\n instanceX: messageData.instanceX,\n instanceY: messageData.instanceY,\n });\n\n if (!instance) {\n // Instance not found, it must have been destroyed already.\n debugLogger.info(\n `Instance ${instanceNetworkId} not found, it must have been destroyed.`\n );\n return;\n }\n\n const behavior = instance.getBehavior(\n 'MultiplayerObject'\n ) as MultiplayerObjectRuntimeBehavior | null;\n if (!behavior) {\n debugLogger.info(\n `Object ${objectName} does not have the MultiplayerObjectBehavior, cannot change ownership.`\n );\n return;\n }\n\n const currentPlayerObjectOwnership = behavior.getPlayerObjectOwnership();\n // Change is coherent if:\n const ownershipChangeIsCoherent =\n // the object is changing ownership from the same owner the host knew about,\n currentPlayerObjectOwnership === previousOwner ||\n // the object is already owned by the new owner. (may have been changed by another player faster)\n currentPlayerObjectOwnership === newOwner;\n if (\n gdjs.multiplayer.isCurrentPlayerHost() &&\n !ownershipChangeIsCoherent\n ) {\n // We received an ownership change message for an object which is in an unexpected state.\n // There may be some lag, and multiple ownership changes may have been sent by the other players.\n // As the host, let's not change the ownership and let the player revert it.\n debugLogger.info(\n `Object ${objectName} with instance network ID ${instanceNetworkId} does not have the expected owner. Wanted to change from ${previousOwner} to ${newOwner}, but object has owner ${currentPlayerObjectOwnership}.`\n );\n return;\n }\n\n // Force the ownership change.\n debugLogger.info(\n `Changing ownership of object ${objectName} to ${newOwner}.`\n );\n behavior.playerNumber = newOwner;\n\n const instanceOwnerChangedMessageName = createInstanceOwnerChangedMessageNameFromChangeInstanceOwnerMessage(\n messageName\n );\n\n debugLogger.info(\n `Sending acknowledgment of ownership change of object ${objectName} from ${previousOwner} to ${newOwner} with instance network ID ${instanceNetworkId} to ${messageSender}.`\n );\n // Once the instance ownership has changed, we need to acknowledge it to the player who sent this message.\n sendDataTo([messageSender], instanceOwnerChangedMessageName, {});\n\n // If we are the host,\n // so we need to relay the ownership change to others,\n // and expect an acknowledgment from them.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n // We don't need to send the message to the player who sent the ownership change message.\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n if (!otherPeerIds.length) {\n // No one else to relay the message to.\n return;\n }\n\n addExpectedMessageAcknowledgement({\n originalMessageName: messageName,\n originalData: messageData,\n expectedMessageName: instanceOwnerChangedMessageName,\n otherPeerIds,\n // As we are the host, we do not cancel the message if it times out.\n shouldCancelMessageIfTimesOut: false,\n });\n debugLogger.info(\n `Relaying ownership change of object ${objectName} with instance network ID ${instanceNetworkId} to ${otherPeerIds.join(\n ', '\n )}.`\n );\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const updateInstanceMessageNamePrefix = '#updateInstance';\n const updateInstanceMessageNameRegex = /#updateInstance#owner_(\\d+)#object_(.+)#instance_(.+)#scene_(.+)/;\n const createUpdateInstanceMessage = ({\n objectOwner,\n objectName,\n instanceNetworkId,\n objectNetworkSyncData,\n sceneNetworkId,\n }: {\n objectOwner: number;\n objectName: string;\n instanceNetworkId: string;\n objectNetworkSyncData: ObjectNetworkSyncData;\n sceneNetworkId: string;\n }): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: `${updateInstanceMessageNamePrefix}#owner_${objectOwner}#object_${objectName}#instance_${instanceNetworkId}#scene_${sceneNetworkId}`,\n messageData: objectNetworkSyncData,\n };\n };\n const handleUpdateInstanceMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Update instance messages do not need to be saved for later use, as the updates are sent pretty often,\n // a new one will be received very quickly.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n\n // When we receive update messages, update the instances in the scene.\n const objectUpdateMessageNames = messageNamesArray.filter((messageName) =>\n messageName.startsWith(updateInstanceMessageNamePrefix)\n );\n objectUpdateMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n\n // For object updates, we start from the newest message, as we want to apply the latest update,\n // and the old messages may have an outdated clock.\n // So we reverse the messages array.\n const reversedMessages = messages.slice().reverse();\n\n reversedMessages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const matches = updateInstanceMessageNameRegex.exec(messageName);\n if (!matches) {\n return;\n }\n const ownerPlayerNumber = parseInt(matches[1], 10);\n if (ownerPlayerNumber === gdjs.multiplayer.playerNumber) {\n // Do not update the instance if we receive an message from ourselves.\n // Should not happen but let's be safe.\n return;\n }\n const objectName = matches[2];\n const instanceNetworkId = matches[3];\n const sceneNetworkId = matches[4];\n\n if (sceneNetworkId !== runtimeScene.networkId) {\n debugLogger.info(\n `Object ${objectName} is in scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n // The object is not in the current scene.\n return;\n }\n\n const messageInstanceClock = messageData['_clock'];\n const lastClock = getLastClockReceivedForInstanceOnScene({\n sceneNetworkId,\n instanceNetworkId,\n });\n\n if (messageInstanceClock <= lastClock) {\n // Ignore old messages, they may be arriving out of order because of lag.\n return;\n }\n\n const instance = getInstanceFromNetworkId({\n runtimeScene,\n objectName,\n instanceNetworkId,\n // This can happen if the object was created on the other player's game, and we need to create it.\n shouldCreateIfNotFound: true,\n instanceX: messageData.x,\n instanceY: messageData.y,\n });\n if (!instance) {\n // This should not happen as we should have created the instance if it did not exist.\n logger.error('Instance could not be found or created.');\n return;\n }\n\n const behavior = instance.getBehavior(\n 'MultiplayerObject'\n ) as MultiplayerObjectRuntimeBehavior | null;\n if (!behavior) {\n logger.error(\n `Object ${objectName} does not have the MultiplayerObjectBehavior, cannot update it.`\n );\n // Object does not have the MultiplayerObjectBehavior, cannot update it.\n return;\n }\n\n // If we receive an update for this object for a different owner than the one we know about,\n // then 2 cases:\n // - If we are the owner of the object, then ignore the message, we assume it's a late update message or a wrong one,\n // we are confident that we own this object. (it may be reverted if we don't receive an acknowledgment in time)\n // - If we are not the owner of the object, then assume that we missed the ownership change message, so update the object's\n // ownership and then update the object.\n if (\n behavior.getPlayerObjectOwnership() ===\n gdjs.multiplayer.playerNumber\n ) {\n debugLogger.info(\n `Object ${objectName} with instance network ID ${instanceNetworkId} is owned by us ${gdjs.multiplayer.playerNumber}, ignoring update message from ${ownerPlayerNumber}.`\n );\n return;\n }\n\n if (behavior.getPlayerObjectOwnership() !== ownerPlayerNumber) {\n debugLogger.info(\n `Object ${objectName} with instance network ID ${instanceNetworkId} is owned by ${behavior.getPlayerObjectOwnership()} on our game, changing ownership to ${ownerPlayerNumber} as part of the update event.`\n );\n behavior.playerNumber = ownerPlayerNumber;\n }\n\n instance.updateFromNetworkSyncData(messageData);\n\n setLastClockReceivedForInstanceOnScene({\n sceneNetworkId,\n instanceNetworkId,\n clock: messageInstanceClock,\n });\n // Also update the clock on the behavior of this instance, so that if we take ownership of this object,\n // we can send the correct clock to the other players.\n behavior._clock = messageInstanceClock;\n\n // If we are are the host,\n // we need to relay the position to others except the player who sent the update message.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n if (!otherPeerIds.length) {\n // No one else to relay the message to.\n return;\n }\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const changeVariableOwnerMessageNamePrefix = '#changeVariableOwner';\n const changeVariableOwnerMessageNameRegex = /#changeVariableOwner#owner_(\\d+)#variable_(.+)/;\n const createChangeVariableOwnerMessage = ({\n variableOwner,\n variableNetworkId,\n newVariableOwner,\n }: {\n variableOwner: number;\n variableNetworkId: string;\n newVariableOwner: number;\n }): {\n messageName: string;\n messageData: {\n previousOwner: number;\n newOwner: number;\n };\n } => {\n return {\n messageName: `${changeVariableOwnerMessageNamePrefix}#owner_${variableOwner}#variable_${variableNetworkId}`,\n messageData: {\n previousOwner: variableOwner,\n newOwner: newVariableOwner,\n },\n };\n };\n const variableOwnerChangedMessageNamePrefix = '#variableOwnerChanged';\n const variableOwnerChangedMessageNameRegex = /#variableOwnerChanged#owner_(\\d+)#variable_(.+)/;\n const createVariableOwnerChangedMessageNameFromChangeVariableOwnerMessage = (\n messageName: string\n ): string => {\n return messageName.replace(\n changeVariableOwnerMessageNamePrefix,\n variableOwnerChangedMessageNamePrefix\n );\n };\n const handleChangeVariableOwnerMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Change owner messages do not need to be saved for later use, as the game will automatically change the owner of\n // the variable when receiving an update message with a different owner.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n\n // When we receive ownership change messages, find the variable and update its ownership.\n const variableOwnershipChangeMessageNames = messageNamesArray.filter(\n (messageName) =>\n messageName.startsWith(changeVariableOwnerMessageNamePrefix)\n );\n variableOwnershipChangeMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const matches = changeVariableOwnerMessageNameRegex.exec(messageName);\n if (!matches) {\n return;\n }\n const variableNetworkId = matches[2];\n const previousOwner = messageData.previousOwner;\n const newOwner = messageData.newOwner;\n\n const {\n type: variableType,\n name: variableName,\n containerId,\n } = gdjs.multiplayerVariablesManager.getVariableTypeAndNameFromNetworkId(\n variableNetworkId\n );\n\n // If this is a scene variable and we are not on the right scene, ignore it.\n if (\n variableType === 'scene' &&\n containerId !== runtimeScene.networkId\n ) {\n debugLogger.info(\n `Variable ${variableName} is in scene ${containerId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n // The variable is not in the current scene.\n return;\n }\n\n const variablesContainer =\n containerId === 'game'\n ? runtimeScene.getGame().getVariables()\n : runtimeScene.getVariables();\n\n if (!variablesContainer.has(variableName)) {\n // Variable not found, this should not happen.\n logger.error(\n `Variable with ID ${variableNetworkId} not found whilst syncing. This should not happen.`\n );\n return;\n }\n\n const variable = variablesContainer.get(variableName);\n\n const currentPlayerVariableOwnership = variable.getPlayerOwnership();\n // Change is coherent if:\n const ownershipChangeIsCoherent =\n // the variable is changing ownership from the same owner the host knew about,\n currentPlayerVariableOwnership === previousOwner ||\n // the variable is already owned by the new owner. (may have been changed by another player faster)\n currentPlayerVariableOwnership === newOwner;\n if (\n gdjs.multiplayer.isCurrentPlayerHost() &&\n !ownershipChangeIsCoherent\n ) {\n // We received an ownership change message for a variable which is in an unexpected state.\n // There may be some lag, and multiple ownership changes may have been sent by the other players.\n // As the host, let's not change the ownership and let the player revert it.\n debugLogger.info(\n `Variable with ID ${variableNetworkId} does not have the expected owner. Wanted to change from ${previousOwner} to ${newOwner}, but variable has owner ${currentPlayerVariableOwnership}.`\n );\n return;\n }\n\n // Force the ownership change.\n debugLogger.info(\n `Changing ownership of variable ${variableName} to ${newOwner}.`\n );\n variable.setPlayerOwnership(newOwner);\n\n const variableOwnerChangedMessageName = createVariableOwnerChangedMessageNameFromChangeVariableOwnerMessage(\n messageName\n );\n\n debugLogger.info(\n `Sending acknowledgment of ownership change of variable with ID ${variableNetworkId} from ${previousOwner} to ${newOwner} to ${messageSender}.`\n );\n // Once the variable ownership has changed, we need to acknowledge it to the player who sent this message.\n sendDataTo([messageSender], variableOwnerChangedMessageName, {});\n\n // If we are the host,\n // we need to relay the ownership change to others,\n // and expect an acknowledgment from them.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n // We don't need to send the message to the player who sent the ownership change message.\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n if (!otherPeerIds.length) {\n // No one else to relay the message to.\n return;\n }\n\n addExpectedMessageAcknowledgement({\n originalMessageName: messageName,\n originalData: messageData,\n expectedMessageName: variableOwnerChangedMessageName,\n otherPeerIds,\n // As we are the host, we do not cancel the message if it times out.\n shouldCancelMessageIfTimesOut: false,\n });\n for (const peerId of otherPeerIds) {\n debugLogger.info(\n `Relaying ownership change of variable with Id ${variableNetworkId} to ${peerId}.`\n );\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n }\n });\n });\n };\n\n const getRegexFromAckMessageName = (messageName: string) => {\n if (messageName.startsWith(instanceDestroyedMessageNamePrefix)) {\n return instanceDestroyedMessageNameRegex;\n } else if (\n messageName.startsWith(instanceOwnerChangedMessageNamePrefix)\n ) {\n return instanceOwnerChangedMessageNameRegex;\n } else if (\n messageName.startsWith(variableOwnerChangedMessageNamePrefix)\n ) {\n return variableOwnerChangedMessageNameRegex;\n } else if (messageName.startsWith(customMessageAcknowledgePrefix)) {\n return customMessageAcknowledgeRegex;\n }\n return null;\n };\n\n const isMessageAcknowledgement = (messageName: string) => {\n return (\n messageName.startsWith(instanceDestroyedMessageNamePrefix) ||\n messageName.startsWith(instanceOwnerChangedMessageNamePrefix) ||\n messageName.startsWith(variableOwnerChangedMessageNamePrefix) ||\n messageName.startsWith(customMessageAcknowledgePrefix)\n );\n };\n\n const handleAcknowledgeMessagesReceived = () => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Acknowledgment messages are mainly a response for ownership change, destruction, and custom messages,\n // which are not sent when the game is not ready.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n // When we receive acknowledgement messages, save it in the extension, to avoid sending the message again.\n const acknowledgedMessageNames = messageNamesArray.filter(\n isMessageAcknowledgement\n );\n acknowledgedMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n debugLogger.info(\n `Received acknowledgment for message ${messageName}.`\n );\n const regex = getRegexFromAckMessageName(messageName);\n if (!regex) {\n // This should not happen.\n logger.error(`Invalid acknowledgment message ${messageName}.`);\n return;\n }\n\n const matches = regex.exec(messageName);\n if (!matches) {\n // This should not happen.\n logger.error(`Invalid acknowledgment message ${messageName}.`);\n return;\n }\n if (!expectedMessageAcknowledgements[messageName]) {\n // This should not happen, but if we receive an acknowledgment for a message we did not expect, let's not error.\n return;\n }\n if (!expectedMessageAcknowledgements[messageName][messageSender]) {\n // This should not happen, but if we receive an acknowledgment from a sender we did not expect, let's not error.\n return;\n }\n\n // If a clock is provided in the message, ensure that we only process the message if the clock is newer than the last one received.\n const messageInstanceClock = messageData['_clock'];\n if (messageInstanceClock !== undefined) {\n const instanceNetworkId = matches[3];\n const sceneNetworkId = matches[4];\n const lastClock = getLastClockReceivedForInstanceOnScene({\n sceneNetworkId,\n instanceNetworkId,\n });\n if (messageInstanceClock <= lastClock) {\n // Ignore old messages.\n return;\n }\n\n setLastClockReceivedForInstanceOnScene({\n sceneNetworkId,\n instanceNetworkId,\n clock: messageInstanceClock,\n });\n }\n\n debugLogger.info(\n `Marking message ${messageName} as acknowledged from ${messageSender}.`\n );\n // Mark the acknowledgment as received.\n expectedMessageAcknowledgements[messageName][\n messageSender\n ].acknowledged = true;\n });\n });\n };\n\n const resendClearOrCancelAcknowledgedMessages = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Acknowledgment messages are mainly a response for ownership change, destruction, and custom messages,\n // which are not sent when the game is not ready.\n return;\n }\n\n // When all acknowledgments are received for an message, we can clear the message from our\n // list of expected acknowledgments.\n const expectedMessageNames = Object.keys(expectedMessageAcknowledgements);\n expectedMessageNames.forEach((acknowledgemessageName) => {\n const acknowledgements =\n expectedMessageAcknowledgements[acknowledgemessageName];\n const peerWhoHaventAcknowledged = Object.keys(acknowledgements).filter(\n (peerId) => !acknowledgements[peerId].acknowledged\n );\n if (!peerWhoHaventAcknowledged.length) {\n // All peers have acknowledged this message, we can clear the object.\n debugLogger.info(\n `All peers have acknowledged message ${acknowledgemessageName}.`\n );\n delete expectedMessageAcknowledgements[acknowledgemessageName];\n } else {\n // Some peers have not acknowledged the message, let's resend it to them.\n for (const peerId of peerWhoHaventAcknowledged) {\n const {\n lastMessageSentAt,\n originalMessageName,\n originalData,\n numberOfRetries: currentNumberOfRetries,\n maxNumberOfRetries,\n messageRetryTime,\n } = acknowledgements[peerId];\n if (getTimeNow() - lastMessageSentAt > messageRetryTime) {\n if (currentNumberOfRetries >= maxNumberOfRetries) {\n // We have retried too many times, let's give up.\n debugLogger.info(\n `Giving up on message ${acknowledgemessageName} for ${peerId}.`\n );\n if (acknowledgements[peerId].shouldCancelMessageIfTimesOut) {\n // If we should cancel the message if it times out, then revert it based on the original message.\n // INSTANCE OWNER CHANGE:\n if (\n originalMessageName.startsWith(\n changeInstanceOwnerMessageNamePrefix\n )\n ) {\n const matches = changeInstanceOwnerMessageNameRegex.exec(\n originalMessageName\n );\n if (!matches) {\n // This should not happen, if it does, remove the acknowledgment and return.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n const objectName = matches[2];\n const instanceNetworkId = matches[3];\n const instances = runtimeScene.getInstancesOf(objectName);\n if (!instances) {\n // object does not exist in the scene, cannot revert ownership.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n let instance = instances.find(\n (instance) => instance.networkId === instanceNetworkId\n );\n if (!instance) {\n // Instance not found, it must have been destroyed already, cannot revert ownership.\n // Should we recreate it?\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n const behavior = instance.getBehavior(\n 'MultiplayerObject'\n ) as MultiplayerObjectRuntimeBehavior | null;\n if (!behavior) {\n logger.error(\n `Object ${objectName} does not have the MultiplayerObjectBehavior, cannot revert ownership.`\n );\n // Object does not have the MultiplayerObjectBehavior, cannot revert ownership.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n const previousOwner = originalData.previousOwner;\n if (previousOwner === undefined) {\n // No previous owner, cannot revert ownership.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n // Force the ownership change.\n behavior.playerNumber = previousOwner || 0;\n }\n\n // VARIABLE OWNER CHANGE:\n if (\n originalMessageName.startsWith(\n changeVariableOwnerMessageNamePrefix\n )\n ) {\n const matches = changeVariableOwnerMessageNameRegex.exec(\n originalMessageName\n );\n if (!matches) {\n // This should not happen, if it does, remove the acknowledgment and return.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n const variableNetworkId = matches[2];\n const previousOwner = originalData.previousOwner;\n\n const {\n type: variableType,\n name: variableName,\n containerId,\n } = gdjs.multiplayerVariablesManager.getVariableTypeAndNameFromNetworkId(\n variableNetworkId\n );\n\n // If this is a scene variable and we are not on the right scene, ignore it.\n if (\n variableType === 'scene' &&\n containerId !== runtimeScene.networkId\n ) {\n debugLogger.info(\n `Variable ${variableName} is in scene ${containerId}, but we are on ${runtimeScene.networkId}. Skipping ownership revert.`\n );\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n const variablesContainer =\n containerId === 'game'\n ? runtimeScene.getGame().getVariables()\n : runtimeScene.getVariables();\n\n if (!variablesContainer.has(variableName)) {\n // Variable not found, this should not happen.\n logger.error(\n `Variable with ID ${variableNetworkId} not found while reverting ownership. This should not happen.`\n );\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n const variable = variablesContainer.get(variableName);\n\n if (previousOwner === undefined) {\n // No previous owner, cannot revert ownership.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n // Force the ownership change.\n variable.setPlayerOwnership(previousOwner || 0);\n }\n }\n delete expectedMessageAcknowledgements[acknowledgemessageName];\n continue;\n }\n\n // We have waited long enough for the acknowledgment, let's resend the message.\n sendDataTo([peerId], originalMessageName, originalData);\n // Reset the timestamp so that we wait again for the acknowledgment.\n acknowledgements[peerId].lastMessageSentAt = getTimeNow();\n // Increment the number of retries.\n acknowledgements[peerId].numberOfRetries =\n currentNumberOfRetries + 1;\n }\n }\n }\n });\n };\n\n const destroyInstanceMessageNamePrefix = '#destroyInstance';\n const destroyInstanceMessageNameRegex = /#destroyInstance#owner_(\\d+)#object_(.+)#instance_(.+)#scene_(.+)/;\n const createDestroyInstanceMessage = ({\n objectOwner,\n objectName,\n instanceNetworkId,\n sceneNetworkId,\n }: {\n objectOwner: number;\n objectName: string;\n instanceNetworkId: string;\n sceneNetworkId: string;\n }): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: `${destroyInstanceMessageNamePrefix}#owner_${objectOwner}#object_${objectName}#instance_${instanceNetworkId}#scene_${sceneNetworkId}`,\n messageData: {},\n };\n };\n const instanceDestroyedMessageNamePrefix = '#instanceDestroyed';\n const instanceDestroyedMessageNameRegex = /#instanceDestroyed#owner_(\\d+)#object_(.+)#instance_(.+)/;\n const createInstanceDestroyedMessageNameFromDestroyInstanceMessage = (\n messageName: string\n ): string => {\n return messageName.replace(\n destroyInstanceMessageNamePrefix,\n instanceDestroyedMessageNamePrefix\n );\n };\n const handleDestroyInstanceMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Destroy messages do not need to be saved for later use, as the game will automatically destroy\n // the instance if it does not receive an update message from it. So we return early.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n const destroyInstanceMessageNames = messageNamesArray.filter(\n (messageName) =>\n messageName.startsWith(destroyInstanceMessageNamePrefix)\n );\n destroyInstanceMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n debugLogger.info(\n `Received message ${messageName} with data ${JSON.stringify(\n messageData\n )}.`\n );\n const matches = destroyInstanceMessageNameRegex.exec(messageName);\n if (!matches) {\n return;\n }\n const playerNumber = parseInt(matches[1], 10);\n if (playerNumber === gdjs.multiplayer.playerNumber) {\n // Do not destroy the object if we receive an message from ourselves.\n // Should probably never happen.\n return;\n }\n const objectName = matches[2];\n const instanceNetworkId = matches[3];\n const sceneNetworkId = matches[4];\n\n if (sceneNetworkId !== runtimeScene.networkId) {\n // The object is not in the current scene.\n debugLogger.info(\n `Object ${objectName} is in scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n return;\n }\n\n const instance = getInstanceFromNetworkId({\n runtimeScene,\n objectName,\n instanceNetworkId,\n });\n\n const instanceDestroyedMessageName = createInstanceDestroyedMessageNameFromDestroyInstanceMessage(\n messageName\n );\n\n if (!instance) {\n debugLogger.info(\n 'Instance was not found in the scene, sending acknowledgment anyway.'\n );\n // Instance not found, it must have been destroyed already.\n // Send an acknowledgment to the player who sent the destroy message in case they missed it.\n sendDataTo([messageSender], instanceDestroyedMessageName, {});\n return;\n }\n\n debugLogger.info(\n `Destroying object ${objectName} with instance network ID ${instanceNetworkId}.`\n );\n instance.deleteFromScene(runtimeScene);\n\n debugLogger.info(\n `Sending acknowledgment of destruction of object ${objectName} with instance network ID ${instanceNetworkId} to ${messageSender}.`\n );\n // Once the object is destroyed, we need to acknowledge it to the player who sent the destroy message.\n sendDataTo([messageSender], instanceDestroyedMessageName, {});\n\n // If we are the host, we need to relay the destruction to others.\n // And expect an acknowledgment from everyone else as well.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n // We don't need to send the message to the player who sent the destroy message.\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n if (!otherPeerIds.length) {\n // No one else to relay the message to.\n return;\n }\n\n addExpectedMessageAcknowledgement({\n originalMessageName: messageName,\n originalData: messageData,\n expectedMessageName: instanceDestroyedMessageName,\n otherPeerIds,\n // As we are the host, we do not cancel the message if it times out.\n shouldCancelMessageIfTimesOut: false,\n });\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const customMessageNamePrefix = '#customMessage';\n const customMessageRegex = /#customMessage#(.+)/;\n const getCustomMessageNameFromUserMessageName = (\n userMessageName: string\n ) => {\n return `${customMessageNamePrefix}#${userMessageName}`;\n };\n const createCustomMessage = ({\n userMessageName,\n userMessageData,\n senderPlayerNumber,\n }: {\n userMessageName: string;\n userMessageData: any;\n senderPlayerNumber: number;\n }) => {\n const messageId = gdjs.makeUuid();\n return {\n messageName: getCustomMessageNameFromUserMessageName(userMessageName),\n messageData: {\n data: userMessageData,\n uniqueId: messageId,\n senderPlayerNumber, // We send the player number, so that other players who are not connected to us can know who sent the message.\n },\n };\n };\n const customMessageAcknowledgePrefix = '#ackCustomMessage';\n const customMessageAcknowledgeRegex = /#ackCustomMessage#(.+)/;\n const createAcknowledgeCustomMessageNameFromCustomMessage = (\n messageName: string\n ): string => {\n return messageName.replace(\n customMessageNamePrefix,\n customMessageAcknowledgePrefix\n );\n };\n\n const sendCustomMessage = (\n userMessageName: string,\n userMessageData: any // can be a simple string message or a serialized variable.\n ) => {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();\n const { messageName, messageData } = createCustomMessage({\n userMessageName,\n userMessageData,\n senderPlayerNumber: currentPlayerNumber,\n });\n const acknowledgmentMessageName = createAcknowledgeCustomMessageNameFromCustomMessage(\n messageName\n );\n addExpectedMessageAcknowledgement({\n originalMessageName: messageName,\n originalData: messageData,\n expectedMessageName: acknowledgmentMessageName,\n otherPeerIds: connectedPeerIds, // Expect acknowledgment from all peers.\n // custom messages cannot be reverted.\n shouldCancelMessageIfTimesOut: false,\n });\n debugLogger.info(\n `Sending custom message ${userMessageName} with data ${JSON.stringify(\n userMessageData\n )}.`\n );\n sendDataTo(connectedPeerIds, messageName, messageData);\n\n // If we are the host, we can consider this messaged as received\n // and add it to the list of custom messages to process on top of the messages received.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const messagesList = gdjs.multiplayerPeerJsHelper.getOrCreateMessagesList(\n messageName\n );\n messagesList.pushMessage(\n messageData,\n gdjs.multiplayerPeerJsHelper.getCurrentId()\n );\n // The message is now automatically added to the list of messages to process,\n // and will be removed at the end of the frame.\n }\n };\n\n const sendVariableCustomMessage = (\n userMessageName: string,\n variable: gdjs.Variable\n ) => {\n const userMessageData = variable.toJSObject();\n debugLogger.info(\n `Sending custom message ${userMessageName} with data ${JSON.stringify(\n userMessageData\n )}.`\n );\n sendCustomMessage(userMessageName, userMessageData);\n };\n\n const hasCustomMessageBeenReceived = (userMessageName: string) => {\n const customMessageName = getCustomMessageNameFromUserMessageName(\n userMessageName\n );\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messagesList = p2pMessagesMap.get(customMessageName);\n if (!messagesList) return; // No message received.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process.\n\n debugLogger.info(`custom message ${userMessageName} has been received.`);\n\n let customMessageHasNotAlreadyBeenProcessed = false;\n\n messages.forEach((message) => {\n const messageData = message.getData();\n const uniqueMessageId = messageData.uniqueId;\n const customMessageCacheKey = `${customMessageName}#${uniqueMessageId}`;\n if (processedCustomMessagesCache.has(customMessageCacheKey)) {\n // Message has already been processed recently. This can happen if the message is sent multiple times,\n // after not being acknowledged properly.\n return;\n }\n processedCustomMessagesCache.add(customMessageCacheKey);\n\n customMessageHasNotAlreadyBeenProcessed = true;\n return;\n });\n\n return customMessageHasNotAlreadyBeenProcessed;\n };\n\n const getCustomMessageData = (userMessageName: string) => {\n const customMessageName = getCustomMessageNameFromUserMessageName(\n userMessageName\n );\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messagesList = p2pMessagesMap.get(customMessageName);\n if (!messagesList) return; // No message received.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process.\n // Assume that the last message is the most recent one.\n const message = messages[messages.length - 1];\n\n const messageData = message.getData();\n return messageData.data;\n };\n\n const getVariableCustomMessageData = (\n userMessageName: string,\n variable: gdjs.Variable\n ) => {\n const data = getCustomMessageData(userMessageName);\n if (!data) {\n return;\n }\n debugLogger.info(\n `Received custom message ${userMessageName} with data ${JSON.stringify(\n data\n )}.`\n );\n variable.fromJSObject(data);\n };\n\n const getCustomMessageSender = (userMessageName: string): number => {\n const customMessageName = getCustomMessageNameFromUserMessageName(\n userMessageName\n );\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messagesList = p2pMessagesMap.get(customMessageName);\n if (!messagesList) return 0;\n const messages = messagesList.getMessages();\n if (!messages.length) return 0;\n // Assume that the last message is the most recent one.\n const message = messages[messages.length - 1];\n const messageData = message.getData();\n\n return messageData.senderPlayerNumber;\n };\n\n const handleCustomMessagesReceived = (): void => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Assume that the custom messages are not worth saving for later use.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n const customMessageNames = messageNamesArray.filter((messageName) =>\n messageName.startsWith(customMessageNamePrefix)\n );\n customMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) {\n logger.error(`No messages list found for ${messageName}.`);\n return; // Should not happen.\n }\n const messages = messagesList.getMessages();\n if (!messages.length) {\n return; // No messages to process for this name.\n }\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const uniqueMessageId = messageData.uniqueId;\n debugLogger.info(\n `Received custom message ${messageName} with data ${JSON.stringify(\n messageData\n )}.`\n );\n const matches = customMessageRegex.exec(messageName);\n if (!matches) {\n // This should not happen.\n logger.error(`Invalid custom message ${messageName}.`);\n return;\n }\n\n const customMessageCacheKey = `${messageName}#${uniqueMessageId}`;\n if (processedCustomMessagesCache.has(customMessageCacheKey)) {\n // Message has already been processed recently. This can happen if the message is sent multiple times,\n // after not being acknowledged properly.\n debugLogger.info(\n `Message ${messageName} has already been processed, skipping.`\n );\n return;\n }\n\n const acknowledgmentMessageName = createAcknowledgeCustomMessageNameFromCustomMessage(\n messageName\n );\n debugLogger.info(\n `Sending acknowledgment of custom message ${messageName} to ${messageSender}.`\n );\n sendDataTo([messageSender], acknowledgmentMessageName, {});\n\n // If we are the host,\n // so we need to relay the message to others.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n // In the case of custom messages, we relay the message to all players, including the sender.\n // This allows the sender to process it the same way others would, when they receive the event.\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n if (!connectedPeerIds.length) {\n // No one else to relay the message to.\n return;\n }\n\n addExpectedMessageAcknowledgement({\n originalMessageName: messageName,\n originalData: messageData,\n expectedMessageName: acknowledgmentMessageName,\n otherPeerIds: connectedPeerIds,\n // As we are the host, we do not cancel the message if it times out.\n shouldCancelMessageIfTimesOut: false,\n });\n sendDataTo(connectedPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const updateSceneMessageNamePrefix = '#updateScene';\n const createUpdateSceneMessage = ({\n sceneNetworkSyncData,\n }: {\n sceneNetworkSyncData: LayoutNetworkSyncData;\n }): {\n messageName: string;\n messageData: LayoutNetworkSyncData;\n } => {\n return {\n messageName: `${updateSceneMessageNamePrefix}`,\n messageData: sceneNetworkSyncData,\n };\n };\n\n const isSceneDifferentFromLastSync = (\n sceneSyncData: LayoutNetworkSyncData\n ) => {\n if (!sceneSyncData.var) {\n return false;\n }\n if (!lastSentSceneSyncData) {\n return true;\n }\n // Compare the json of the scene sync data to know if it has changed.\n // Not the most efficient way, but it's good enough for now.\n const haveVariableSyncDataChanged =\n JSON.stringify(sceneSyncData.var) !==\n JSON.stringify(lastSentSceneSyncData.var);\n\n return haveVariableSyncDataChanged;\n };\n\n const hasSceneBeenSyncedRecently = () => {\n return (\n getTimeNow() - lastSceneSyncTimestamp < 1000 / sceneSyncDataSyncRate\n );\n };\n\n const handleUpdateSceneMessagesToSend = (\n runtimeScene: gdjs.RuntimeScene\n ): void => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Don't send messages if the multiplayer is not ready.\n return;\n }\n\n const sceneNetworkSyncData = runtimeScene.getNetworkSyncData({\n playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),\n isHost: gdjs.multiplayer.isCurrentPlayerHost(),\n });\n if (!sceneNetworkSyncData) {\n return;\n }\n\n const isSceneSyncDataDifferent = isSceneDifferentFromLastSync(\n sceneNetworkSyncData\n );\n const shouldSyncScene =\n !hasSceneBeenSyncedRecently() ||\n isSceneSyncDataDifferent ||\n numberOfForcedSceneUpdates > 0;\n\n if (isSceneSyncDataDifferent) {\n numberOfForcedSceneUpdates = 3;\n }\n\n if (!shouldSyncScene) {\n return;\n }\n\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createUpdateSceneMessage({\n sceneNetworkSyncData,\n });\n\n sendDataTo(connectedPeerIds, messageName, messageData);\n\n lastSceneSyncTimestamp = getTimeNow();\n lastSentSceneSyncData = sceneNetworkSyncData;\n numberOfForcedSceneUpdates = Math.max(numberOfForcedSceneUpdates - 1, 0);\n };\n\n const handleUpdateSceneMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n const updateSceneMessageNames = messageNamesArray.filter((messageName) =>\n messageName.startsWith(updateSceneMessageNamePrefix)\n );\n updateSceneMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const sceneNetworkId = messageData.id;\n\n if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n if (sceneNetworkId !== runtimeScene.networkId) {\n debugLogger.info(\n `Received update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n // The scene is not the current scene.\n return;\n }\n\n runtimeScene.updateFromNetworkSyncData(messageData);\n } else {\n // If the game is not ready to receive game update messages, we need to save the data for later use.\n // This can happen when joining a game that is already running.\n debugLogger.info(\n `Saving scene ${sceneNetworkId} update message for later use.`\n );\n lastReceivedSceneSyncDataUpdates.store(messageData);\n return;\n }\n\n // If we are are the host,\n // we need to relay the scene update to others except the player who sent the update message.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n // We don't need to send the message to the player who sent the update message.\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const updateGameMessageNamePrefix = '#updateGame';\n const createUpdateGameMessage = ({\n gameNetworkSyncData,\n }: {\n gameNetworkSyncData: GameNetworkSyncData;\n }): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: `${updateGameMessageNamePrefix}`,\n messageData: gameNetworkSyncData,\n };\n };\n const isGameDifferentFromLastSync = (gameSyncData: GameNetworkSyncData) => {\n const variablesToSync = gameSyncData.var;\n const sceneStackToSync = gameSyncData.ss;\n if (!variablesToSync && !sceneStackToSync) {\n // Nothing to sync.\n return false;\n }\n\n if (\n !lastSentGameSyncData ||\n !lastSentGameSyncData.var ||\n !lastSentGameSyncData.ss\n ) {\n // We have not sent any game sync data yet, probably start of the game, let's do it.\n return true;\n }\n\n // Compare the json of the game variables sync data to know if it has changed.\n // Not the most efficient way, but it's good enough for now.\n if (\n variablesToSync &&\n JSON.stringify(variablesToSync) !==\n JSON.stringify(lastSentGameSyncData.var)\n ) {\n return true;\n }\n\n // For the sceneStack, loop through them one by one as it's more efficient.\n if (sceneStackToSync) {\n // If the length has changed, we're sure it's different.\n if (sceneStackToSync.length !== lastSentGameSyncData.ss.length) {\n return true;\n }\n\n for (let i = 0; i < sceneStackToSync.length; ++i) {\n const sceneToSync = sceneStackToSync[i];\n const lastSceneSent = lastSentGameSyncData.ss[i];\n if (\n sceneToSync.name !== lastSceneSent.name ||\n sceneToSync.networkId !== lastSceneSent.networkId\n ) {\n return true;\n }\n }\n }\n\n return false;\n };\n\n const hasGameBeenSyncedRecently = () => {\n return getTimeNow() - lastGameSyncTimestamp < 1000 / gameSyncDataSyncRate;\n };\n\n const handleUpdateGameMessagesToSend = (\n runtimeScene: gdjs.RuntimeScene\n ): void => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Don't send messages if the multiplayer is not ready.\n return;\n }\n\n const gameNetworkSyncData = runtimeScene.getGame().getNetworkSyncData({\n playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),\n isHost: gdjs.multiplayer.isCurrentPlayerHost(),\n });\n if (!gameNetworkSyncData) {\n return;\n }\n\n const isGameSyncDataDifferent = isGameDifferentFromLastSync(\n gameNetworkSyncData\n );\n const shouldSyncGame =\n !hasGameBeenSyncedRecently() ||\n isGameSyncDataDifferent ||\n numberOfForcedGameUpdates > 0;\n\n if (isGameSyncDataDifferent) {\n numberOfForcedGameUpdates = 3;\n }\n\n if (!shouldSyncGame) {\n return;\n }\n\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createUpdateGameMessage({\n gameNetworkSyncData,\n });\n\n sendDataTo(connectedPeerIds, messageName, messageData);\n\n lastGameSyncTimestamp = getTimeNow();\n lastSentGameSyncData = gameNetworkSyncData;\n numberOfForcedGameUpdates = Math.max(numberOfForcedGameUpdates - 1, 0);\n };\n\n const handleUpdateGameMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n const updateGameMessageNames = messageNamesArray.filter((messageName) =>\n messageName.startsWith(updateGameMessageNamePrefix)\n );\n updateGameMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n runtimeScene.getGame().updateFromNetworkSyncData(messageData);\n } else {\n // If the game is not ready to receive game update messages, we need to save the data for later use.\n // This can happen when joining a game that is already running.\n debugLogger.info(`Saving game update message for later use.`);\n lastReceivedGameSyncDataUpdates.store(messageData);\n return;\n }\n\n // If we are are the host,\n // we need to relay the game update to others except the player who sent the update message.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n // We don't need to send the message to the player who sent the update message.\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const handleSavedUpdateMessages = (runtimeScene: gdjs.RuntimeScene) => {\n // Reapply the game saved updates.\n lastReceivedGameSyncDataUpdates.getUpdates().forEach((messageData) => {\n debugLogger.info(`Reapplying saved update of game.`);\n runtimeScene.getGame().updateFromNetworkSyncData(messageData);\n });\n // Game updates are always applied properly, so we can clear them.\n lastReceivedGameSyncDataUpdates.clear();\n\n // Then reapply the scene saved updates.\n lastReceivedSceneSyncDataUpdates.getUpdates().forEach((messageData) => {\n const sceneNetworkId = messageData.id;\n\n if (sceneNetworkId !== runtimeScene.networkId) {\n debugLogger.info(\n `Trying to apply saved update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n // The scene is not the current scene.\n return;\n }\n\n debugLogger.info(`Reapplying saved update of scene ${sceneNetworkId}.`);\n\n runtimeScene.updateFromNetworkSyncData(messageData);\n // We only remove the message if it was successfully applied, so it can be reapplied later,\n // in case we were not on the right scene.\n lastReceivedSceneSyncDataUpdates.remove(messageData);\n });\n };\n\n const heartbeatMessageNamePrefix = '#heartbeat';\n const heartbeastMessageRegex = /#heartbeat#(.+)/;\n const createHeartbeatMessage = (): {\n messageName: string;\n messageData: any;\n } => {\n // If we create the heartbeat meassage, we are the host,\n // Ensure our player number is correctly set when the first heartbeat is sent.\n _playersInfo[gdjs.multiplayer.getCurrentPlayerNumber()] = {\n ping: 0, // we are the host, so we don't need to compute the ping.\n playerId: gdjs.playerAuthentication.getUserId(),\n username: gdjs.playerAuthentication.getUsername(),\n };\n for (const playerNumber in _playersInfo) {\n _playersInfo[playerNumber] = {\n ..._playersInfo[playerNumber],\n ping: getPlayerPing(parseInt(playerNumber, 10)),\n };\n }\n return {\n messageName: `${heartbeatMessageNamePrefix}#${gdjs.multiplayer.getCurrentPlayerNumber()}`,\n messageData: {\n now: getTimeNow(), // we send the current time to compute the ping.\n playersInfo: _playersInfo,\n },\n };\n };\n const createHeartbeatAnswerMessage = ({\n heartbeatSentAt,\n }: {\n heartbeatSentAt: number;\n }): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: `${heartbeatMessageNamePrefix}#${gdjs.multiplayer.getCurrentPlayerNumber()}`,\n messageData: {\n sentAt: heartbeatSentAt,\n playerId: gdjs.playerAuthentication.getUserId(),\n username: gdjs.playerAuthentication.getUsername(),\n },\n };\n };\n const hasSentHeartbeatRecently = () => {\n return (\n !!lastHeartbeatSentTimestamp &&\n getTimeNow() - lastHeartbeatSentTimestamp < 1000 / heartbeatSyncRate\n );\n };\n const handleHeartbeatsToSend = () => {\n // Only host sends heartbeats to all players regularly:\n // - it allows them to send a heartbeat back immediately so that the host can compute the ping.\n // - it allows to pass along the pings of all players to all players.\n if (!gdjs.multiplayer.isCurrentPlayerHost()) {\n return;\n }\n\n const shouldSendHeartbeat = !hasSentHeartbeatRecently();\n if (!shouldSendHeartbeat) {\n return;\n }\n\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createHeartbeatMessage();\n sendDataTo(connectedPeerIds, messageName, messageData);\n\n lastHeartbeatSentTimestamp = getTimeNow();\n };\n\n const handleHeartbeatsReceived = () => {\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n const heartbeatMessageNames = messageNamesArray.filter((messageName) =>\n messageName.startsWith(heartbeatMessageNamePrefix)\n );\n heartbeatMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const matches = heartbeastMessageRegex.exec(messageName);\n if (!matches) {\n return;\n }\n const playerNumber = parseInt(matches[1], 10);\n // Ensure we know who is who.\n _peerIdToPlayerNumber[messageSender] = playerNumber;\n\n // If we are not the host, save what the host told us about the other players info\n // and respond with a heartbeat immediately, informing the host of our playerId and username.\n if (!gdjs.multiplayer.isCurrentPlayerHost()) {\n const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();\n const currentlyKnownPlayerNumbers = Object.keys(\n _playersInfo\n ).map((playerNumber) => parseInt(playerNumber, 10));\n const receivedPlayerNumbers = Object.keys(\n messageData.playersInfo\n ).map((playerNumber) => parseInt(playerNumber, 10));\n const currentlyKnownPingForCurrentUser =\n _playersInfo[currentPlayerNumber] &&\n _playersInfo[currentPlayerNumber].ping;\n // If there are no players info yet, we're probably just connecting.\n // This can happen when joining a game that is already running.\n // Do not handle this case to avoid displaying too many notifications.\n if (!!currentlyKnownPlayerNumbers.length) {\n // Look at the players info received to know if there are new players who just connected.\n const newPlayerNumbers = receivedPlayerNumbers.filter(\n (playerNumber) =>\n !currentlyKnownPlayerNumbers.includes(playerNumber) &&\n playerNumber !== currentPlayerNumber // Do not consider ourselves as a new player.\n );\n _playerNumbersWhoJustJoined.push(...newPlayerNumbers);\n // Or players who have disconnected.\n const playerNumbersWhoHaveDisconnected = currentlyKnownPlayerNumbers.filter(\n (playerNumber) => !receivedPlayerNumbers.includes(playerNumber)\n );\n _playerNumbersWhoJustLeft.push(\n ...playerNumbersWhoHaveDisconnected\n );\n for (const playerNumber of playerNumbersWhoHaveDisconnected) {\n // Temporarily save the username in another variable to be used for the notification,\n // as we're deleting its playerInfo just after.\n _temporaryPlayerNumberToUsername[\n playerNumber\n ] = getPlayerUsername(playerNumber);\n }\n }\n\n // Save the players info received from the host.\n // Avoid overwriting the whole object as it can mess up tests that rely on the object reference.\n cloneObjectWithoutOverwriting({\n source: messageData.playersInfo,\n target: _playersInfo,\n });\n\n const {\n messageName: answerMessageName,\n messageData: answerMessageData,\n } = createHeartbeatAnswerMessage({\n heartbeatSentAt: messageData.now, // We send back the time we received, so that the host can compute the ping.\n });\n sendDataTo([messageSender], answerMessageName, answerMessageData);\n // We have received a heartbeat from the host, informing us of our ping,\n // so we can consider the connection as working.\n if (\n _playersInfo[currentPlayerNumber] !== undefined &&\n _playersInfo[currentPlayerNumber].ping !== undefined\n ) {\n gdjs.multiplayer.markConnectionAsConnected();\n if (currentlyKnownPingForCurrentUser === undefined) {\n // We just connected, let's add ourselves to the list of players who just connected,\n // for the notification and the events.\n _playerNumbersWhoJustJoined.push(currentPlayerNumber);\n }\n }\n\n return;\n }\n\n // If we are the host.\n\n // If this is a new player, we're about to send them their ping, so we can consider them connected.\n if (!_playersInfo[playerNumber]) {\n _playerNumbersWhoJustJoined.push(playerNumber);\n }\n\n // compute the pings based on:\n // - the time we received the heartbeat.\n // - the time the heartbeat was sent.\n const now = getTimeNow();\n const heartbeatSentAt = messageData.sentAt;\n const roundTripTime = Math.round(now - heartbeatSentAt);\n const playerLastRoundTripTimes =\n _playersLastRoundTripTimes[playerNumber] || [];\n playerLastRoundTripTimes.push(roundTripTime);\n if (playerLastRoundTripTimes.length > 5) {\n // Keep only the last 5 RTT to compute the average.\n playerLastRoundTripTimes.shift();\n }\n _playersLastRoundTripTimes[playerNumber] = playerLastRoundTripTimes;\n\n let sum = 0;\n for (const lastRoundTripTime of playerLastRoundTripTimes) {\n sum += lastRoundTripTime;\n }\n const averagePing = Math.round(\n sum / playerLastRoundTripTimes.length / 2 // Divide by 2 to get the one way ping.\n );\n _playersInfo[playerNumber] = {\n ping: averagePing,\n playerId: messageData.playerId,\n username: messageData.username,\n };\n\n // If there are new players, let's resend a heartbeat right away so that everyone is aware of them\n // on approximately the same frame.\n if (_playerNumbersWhoJustJoined.length) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createHeartbeatMessage();\n sendDataTo(connectedPeerIds, messageName, messageData);\n lastHeartbeatSentTimestamp = getTimeNow();\n }\n });\n });\n };\n\n const hasReceivedHeartbeatFromPlayer = (playerNumber: number) => {\n // Consider that a player has sent a heartbeat if we have been able to calculate\n // at least one round trip time for them.\n const playerLastRoundTripTimes =\n _playersLastRoundTripTimes[playerNumber] || [];\n return playerLastRoundTripTimes.length > 0;\n };\n\n const getPlayerPing = (playerNumber: number) => {\n const playerInfo = _playersInfo[playerNumber];\n if (!playerInfo) {\n return 0;\n }\n return playerInfo.ping || 0;\n };\n\n const getCurrentPlayerPing = () => {\n const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();\n return getPlayerPing(currentPlayerNumber);\n };\n\n const markPlayerAsDisconnected = ({\n runtimeScene,\n playerNumber,\n peerId,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n playerNumber: number;\n peerId?: string;\n }) => {\n logger.info(`Marking player ${playerNumber} as disconnected.`);\n _playerNumbersWhoJustLeft.push(playerNumber);\n // Temporarily save the username in another variable to be used for the notification,\n // as we're deleting its playerInfo just after.\n _temporaryPlayerNumberToUsername[playerNumber] = getPlayerUsername(\n playerNumber\n );\n clearPlayerTempData(playerNumber);\n\n // If Host has disconnected, either switch host or stop the game.\n if (peerId && peerId === gdjs.multiplayer.hostPeerId) {\n const shouldEndLobbyGame = gdjs.multiplayer.shouldEndLobbyWhenHostLeaves();\n if (shouldEndLobbyGame) {\n logger.info('Host has disconnected, ending the game.');\n\n clearAllMessagesTempData();\n gdjs.multiplayer.handleLobbyGameEnded();\n } else {\n logger.info('Host has disconnected, switching host.');\n\n gdjs.multiplayer.handleHostDisconnected({ runtimeScene });\n return;\n }\n }\n\n // If we are the host, send a heartbeat right away so that everyone is aware of the disconnection\n // on approximately the same frame.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createHeartbeatMessage();\n sendDataTo(connectedPeerIds, messageName, messageData);\n lastHeartbeatSentTimestamp = getTimeNow();\n }\n };\n\n const getPlayerUsername = (playerNumber: number) => {\n return (\n (_playersInfo[playerNumber] || {}).username ||\n _temporaryPlayerNumberToUsername[playerNumber] ||\n `Player ${playerNumber}`\n );\n };\n\n const getPlayerId = (playerNumber: number) => {\n return (_playersInfo[playerNumber] || {}).playerId || '';\n };\n\n const handleJustDisconnectedPeers = (runtimeScene: RuntimeScene) => {\n // If the game is not running, we don't need to handle disconnected peers.\n if (!gdjs.multiplayer.isLobbyGameRunning()) {\n return;\n }\n\n // We rely on the p2p helper to know who has disconnected.\n const justDisconnectedPlayers: {\n playerNumber: number;\n peerId: string;\n }[] = [];\n\n const justDisconnectedPeers = gdjs.multiplayerPeerJsHelper.getJustDisconnectedPeers();\n if (justDisconnectedPeers.length) {\n for (const disconnectedPeer of justDisconnectedPeers) {\n const disconnectedPlayerNumber =\n _peerIdToPlayerNumber[disconnectedPeer];\n if (!disconnectedPlayerNumber) {\n // This should not happen.\n return;\n }\n logger.info(`Player ${disconnectedPlayerNumber} has disconnected.`);\n justDisconnectedPlayers.push({\n playerNumber: disconnectedPlayerNumber,\n peerId: disconnectedPeer,\n });\n }\n }\n\n for (const { playerNumber, peerId } of justDisconnectedPlayers) {\n // When a player disconnects, as the host, we look at all the instances\n // they own and decide what to do with them.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const instances = runtimeScene.getAdhocListOfAllInstances();\n for (const instance of instances) {\n const behavior = instance.getBehavior(\n 'MultiplayerObject'\n ) as MultiplayerObjectRuntimeBehavior | null;\n if (\n behavior &&\n behavior.getPlayerObjectOwnership() === playerNumber\n ) {\n const actionOnPlayerDisconnect = behavior.getActionOnPlayerDisconnect();\n if (actionOnPlayerDisconnect === 'DestroyObject') {\n // No need to remove the ownership, as the destroy message will be sent to all players.\n instance.deleteFromScene(runtimeScene);\n } else if (actionOnPlayerDisconnect === 'GiveOwnershipToHost') {\n // Removing the ownership will send a message to all players.\n behavior.removeObjectOwnership();\n } else if (actionOnPlayerDisconnect === 'DoNothing') {\n // Do nothing.\n }\n }\n }\n }\n\n markPlayerAsDisconnected({ runtimeScene, playerNumber, peerId });\n }\n };\n\n const hasAnyPlayerJustLeft = (): boolean => {\n return _playerNumbersWhoJustLeft.length > 0;\n };\n const hasPlayerJustLeft = (playerNumber: number): boolean => {\n return _playerNumbersWhoJustLeft.includes(playerNumber);\n };\n const getPlayersWhoJustLeft = (): number[] => {\n return _playerNumbersWhoJustLeft;\n };\n const getLatestPlayerWhoJustLeft = (): number => {\n return _playerNumbersWhoJustLeft[0] || 0;\n };\n const removePlayerWhoJustLeft = (): void => {\n // Avoid using shift for test purposes, as it modifies the reference.\n const playerNumberWhoLeft = _playerNumbersWhoJustLeft[0];\n if (playerNumberWhoLeft !== undefined) {\n _playerNumbersWhoJustLeft = _playerNumbersWhoJustLeft.slice(1);\n delete _temporaryPlayerNumberToUsername[playerNumberWhoLeft];\n }\n };\n\n const hasAnyPlayerJustJoined = () => {\n return _playerNumbersWhoJustJoined.length > 0;\n };\n const hasPlayerJustJoined = (playerNumber: number): boolean => {\n return _playerNumbersWhoJustJoined.includes(playerNumber);\n };\n const getPlayersWhoJustJoined = () => {\n return _playerNumbersWhoJustJoined;\n };\n const getLatestPlayerWhoJustJoined = (): number => {\n return _playerNumbersWhoJustJoined[0] || 0;\n };\n const removePlayerWhoJustJoined = (): void => {\n // Avoid using shift for test purposes, as it modifies the reference.\n const playerNumberWhoJoined = _playerNumbersWhoJustJoined[0];\n if (playerNumberWhoJoined !== undefined) {\n _playerNumbersWhoJustJoined = _playerNumbersWhoJustJoined.slice(1);\n }\n };\n\n const getConnectedPlayers = () => {\n return Object.keys(_playersInfo).map((playerNumber) => ({\n playerNumber: parseInt(playerNumber, 10),\n playerId: _playersInfo[playerNumber].playerId,\n }));\n };\n const getNumberOfConnectedPlayers = () => {\n // Look at the player info as a way to know how many players are in the lobby.\n // This object is updated when heartbeats are sent and received.\n return Object.keys(_playersInfo).length;\n };\n const isPlayerConnected = (playerNumber: number) => {\n return _playersInfo[playerNumber] !== undefined;\n };\n\n const getPlayersInfo = () => {\n return _playersInfo;\n };\n\n const endGameMessageName = '#endGame';\n const createEndGameMessage = (): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: endGameMessageName,\n messageData: {},\n };\n };\n const sendEndGameMessage = () => {\n // Only the host can end the game.\n if (!gdjs.multiplayer.isCurrentPlayerHost()) {\n return;\n }\n\n debugLogger.info(`Sending endgame message.`);\n\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createEndGameMessage();\n // Note: we don't wait for an acknowledgment here, as the game will end anyway.\n sendDataTo(connectedPeerIds, messageName, messageData);\n };\n\n const handleEndGameMessagesReceived = () => {\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n // Only other players need to react to the end game message.\n return;\n }\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const endGameMessagesList = p2pMessagesMap.get(endGameMessageName);\n if (!endGameMessagesList) {\n return; // No end game message received.\n }\n const messages = endGameMessagesList.getMessages();\n if (!messages.length) return; // No messages to process.\n\n logger.info(`Received endgame message.`);\n\n // If the message is received more than 1 time, we just ignore it and end the game.\n\n clearAllMessagesTempData();\n gdjs.multiplayer.handleLobbyGameEnded();\n };\n\n const resumeGameMessageName = '#resumeGame';\n const createResumeGameMessage = (): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: resumeGameMessageName,\n messageData: {},\n };\n };\n const sendResumeGameMessage = () => {\n // Only the host can inform others that the game is resuming.\n if (!gdjs.multiplayer.isCurrentPlayerHost()) {\n return;\n }\n\n debugLogger.info(`Sending resumeGame message.`);\n\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createResumeGameMessage();\n sendDataTo(connectedPeerIds, messageName, messageData);\n };\n\n const handleResumeGameMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n // Only other players need to react to resume game message.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const resumeGameMessagesList = p2pMessagesMap.get(resumeGameMessageName);\n if (!resumeGameMessagesList) {\n return; // No resume game message received.\n }\n const messages = resumeGameMessagesList.getMessages();\n if (!messages.length) return; // No messages to process.\n\n logger.info(`Received resumeGame message.`);\n\n gdjs.multiplayer.resumeGame(runtimeScene);\n };\n\n const clearAllMessagesTempData = () => {\n _playersLastRoundTripTimes = {};\n _playersInfo = {};\n lastReceivedGameSyncDataUpdates.clear();\n lastReceivedSceneSyncDataUpdates.clear();\n processedCustomMessagesCache.clear();\n _playerNumbersWhoJustLeft = [];\n _playerNumbersWhoJustJoined = [];\n expectedMessageAcknowledgements = {};\n _lastClockReceivedByInstanceByScene = {};\n };\n\n const clearPlayerTempData = (playerNumber: number) => {\n // Remove the player from the list of players.\n // This will cause the next hearbeat to not include this player\n // and the others will consider them as disconnected.\n delete _playersLastRoundTripTimes[playerNumber];\n delete _playersInfo[playerNumber];\n };\n\n return {\n sendDataTo,\n // Acks.\n addExpectedMessageAcknowledgement,\n handleAcknowledgeMessagesReceived,\n resendClearOrCancelAcknowledgedMessages,\n // Instance ownership.\n createChangeInstanceOwnerMessage,\n createInstanceOwnerChangedMessageNameFromChangeInstanceOwnerMessage,\n handleChangeInstanceOwnerMessagesReceived,\n // Instance update.\n createUpdateInstanceMessage,\n handleUpdateInstanceMessagesReceived,\n // Instance destruction.\n createDestroyInstanceMessage,\n createInstanceDestroyedMessageNameFromDestroyInstanceMessage,\n handleDestroyInstanceMessagesReceived,\n // Variable ownership.\n createChangeVariableOwnerMessage,\n createVariableOwnerChangedMessageNameFromChangeVariableOwnerMessage,\n handleChangeVariableOwnerMessagesReceived,\n // Custom messages.\n sendCustomMessage,\n getCustomMessageData,\n sendVariableCustomMessage,\n getVariableCustomMessageData,\n hasCustomMessageBeenReceived,\n handleCustomMessagesReceived,\n getCustomMessageSender,\n // Scene update.\n createUpdateSceneMessage,\n handleUpdateSceneMessagesToSend,\n handleUpdateSceneMessagesReceived,\n // Game update.\n createUpdateGameMessage,\n handleUpdateGameMessagesToSend,\n handleUpdateGameMessagesReceived,\n handleSavedUpdateMessages,\n // Heartbeats.\n handleHeartbeatsToSend,\n handleHeartbeatsReceived,\n hasReceivedHeartbeatFromPlayer,\n // Pings & usernames.\n getPlayerPing,\n getCurrentPlayerPing,\n getPlayerUsername,\n getPlayerId,\n // Connected players.\n handleJustDisconnectedPeers,\n getConnectedPlayers,\n getNumberOfConnectedPlayers,\n isPlayerConnected,\n getPlayersInfo,\n // Leaving players.\n hasAnyPlayerJustLeft,\n hasPlayerJustLeft,\n getPlayersWhoJustLeft,\n getLatestPlayerWhoJustLeft,\n removePlayerWhoJustLeft,\n markPlayerAsDisconnected,\n // Joining players.\n hasAnyPlayerJustJoined,\n hasPlayerJustJoined,\n getPlayersWhoJustJoined,\n getLatestPlayerWhoJustJoined,\n removePlayerWhoJustJoined,\n // End game.\n sendEndGameMessage,\n handleEndGameMessagesReceived,\n clearAllMessagesTempData,\n // Resume game after migration.\n sendResumeGameMessage,\n handleResumeGameMessagesReceived,\n };\n };\n\n /**\n * The MultiplayerMessageManager used by the game.\n */\n export let multiplayerMessageManager = makeMultiplayerMessageManager();\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["namespace gdjs {\n const logger = new gdjs.Logger('Multiplayer');\n const debugLogger = new gdjs.Logger('Multiplayer - Debug');\n // Comment this to see message logs and ease debugging:\n gdjs.Logger.getDefaultConsoleLoggerOutput().discardGroup(\n 'Multiplayer - Debug'\n );\n\n class RecentlySeenKeys {\n maxSize: number;\n cache: Set<string>;\n keys: string[];\n\n constructor(maxSize: number) {\n this.maxSize = maxSize;\n this.cache = new Set();\n this.keys = [];\n }\n\n has(key: string) {\n return this.cache.has(key);\n }\n\n add(key: string) {\n // If we are at the maximum size, remove the first key.\n if (this.cache.size >= this.maxSize) {\n const keyToRemove = this.keys.shift();\n if (keyToRemove) {\n this.cache.delete(keyToRemove);\n }\n }\n\n // Add the key to the end of the list.\n this.cache.add(key);\n this.keys.push(key);\n }\n\n clear = () => {\n this.cache.clear();\n this.keys = [];\n };\n }\n\n class SavedSyncDataUpdates<T> {\n private _updates: T[] = [];\n\n store(update: T) {\n this._updates.push(update);\n if (this._updates.length > 10) {\n this._updates.shift();\n }\n }\n\n getUpdates() {\n return this._updates;\n }\n\n remove(update: T) {\n const index = this._updates.indexOf(update);\n if (index !== -1) {\n this._updates.splice(index, 1);\n }\n }\n\n clear() {\n this._updates = [];\n }\n }\n\n /**\n * Helper function to clone an object without reassigning the target object.\n * It's mainly helpful for tests, where multiple instances of the MultiplayerMessageManager are created,\n * and prevents keeping references to the same object.\n */\n const cloneObjectWithoutOverwriting = ({\n target,\n source,\n }: {\n target: Object;\n source: Object;\n }) => {\n // Add the new properties.\n for (const key in source) {\n if (source.hasOwnProperty(key) && !target.hasOwnProperty(key)) {\n target[key] = source[key];\n }\n }\n\n // Remove the properties that are not in the source.\n for (const key in target) {\n if (target.hasOwnProperty(key) && !source.hasOwnProperty(key)) {\n delete target[key];\n }\n }\n };\n\n export type MultiplayerMessageManager = ReturnType<\n typeof makeMultiplayerMessageManager\n >;\n\n /**\n * Create a new MultiplayerMessageManager.\n *\n * In most cases, you should use the default `gdjs.multiplayerMessageManager` instead.\n *\n * @returns\n */\n export const makeMultiplayerMessageManager = () => {\n // For testing purposes, you can simulate network latency and packet loss.\n // Adds x ms to all network messages, simulating a slow network.\n const SIMULATE_NETWORK_LATENCY_MS = 0; // In ms.\n // Gives a random chance of packet loss, simulating a bad network.\n const SIMULATE_NETWORK_PACKET_LOSS_CHANCE = 0; // Between 0 and 1, % of packets lost.\n // Adds a latency to random network messages, simulating sporadic network issues.\n const SIMULATE_NETWORK_RANDOM_SLOW_PACKET_CHANCE = 0; // Between 0 and 1, % of packets that will be slow.\n const SIMULATE_NETWORK_RANDOM_LATENCY_MS = 0; // In ms.\n\n const getTimeNow =\n window.performance && typeof window.performance.now === 'function'\n ? window.performance.now.bind(window.performance)\n : Date.now;\n const defaultMessageRetryTime = 200; // Time to wait before retrying a message that was not acknowledged, in ms.\n const defaultMaxRetries = 4; // Maximum number of retries before giving up on a message.\n\n // Make the processed messages an LRU cache, so that we can limit the number of messages we keep in memory,\n // as well as keep them in order.\n const processedCustomMessagesCache = new RecentlySeenKeys(500);\n\n let expectedMessageAcknowledgements: {\n [messageName: string]: {\n [peerId: string]: {\n acknowledged: boolean;\n lastMessageSentAt: number;\n originalMessageName: string;\n originalData: any;\n numberOfRetries: number;\n maxNumberOfRetries: number;\n messageRetryTime: number;\n shouldCancelMessageIfTimesOut?: boolean;\n };\n };\n } = {};\n let _lastClockReceivedByInstanceByScene: {\n [sceneId: string]: { [instanceId: string]: number };\n } = {};\n\n // The number of times per second the scene data should be synchronized.\n const sceneSyncDataSyncRate = 1;\n let lastSceneSyncTimestamp = 0;\n let lastSentSceneSyncData: LayoutNetworkSyncData | null = null;\n let numberOfForcedSceneUpdates = 0;\n let lastReceivedSceneSyncDataUpdates = new SavedSyncDataUpdates<\n LayoutNetworkSyncData\n >();\n\n // The number of times per second the game data should be synchronized.\n const gameSyncDataSyncRate = 1;\n let lastGameSyncTimestamp = 0;\n let lastSentGameSyncData: GameNetworkSyncData | null = null;\n let numberOfForcedGameUpdates = 0;\n let lastReceivedGameSyncDataUpdates = new SavedSyncDataUpdates<\n GameNetworkSyncData\n >();\n\n // Send heartbeat messages from host to players, ensuring their connection is still alive,\n // measure the ping, and send other useful info.\n const heartbeatSyncRate = 1;\n let lastHeartbeatSentTimestamp = 0;\n let _playersLastRoundTripTimes: {\n [playerNumber: number]: number[];\n } = {};\n let _peerIdToPlayerNumber: { [peerId: string]: number } = {};\n let _playersInfo: {\n [playerNumber: number]: {\n ping: number;\n playerId: string;\n username: string;\n };\n } = {};\n let _playerNumbersWhoJustLeft: number[] = [];\n let _playerNumbersWhoJustJoined: number[] = [];\n let _temporaryPlayerNumberToUsername: {\n [playerNumber: number]: string;\n } = {};\n\n const addExpectedMessageAcknowledgement = ({\n originalMessageName,\n originalData,\n expectedMessageName,\n otherPeerIds,\n shouldCancelMessageIfTimesOut,\n maxNumberOfRetries,\n messageRetryTime,\n }: {\n originalMessageName: string;\n originalData: any;\n expectedMessageName: string;\n otherPeerIds: string[];\n shouldCancelMessageIfTimesOut: boolean;\n maxNumberOfRetries?: number;\n messageRetryTime?: number;\n }) => {\n if (!gdjs.multiplayer.isLobbyGameRunning()) {\n // This can happen if objects are destroyed at the end of the scene.\n // We should not add expected messages in this case.\n return;\n }\n\n if (!expectedMessageAcknowledgements[expectedMessageName]) {\n expectedMessageAcknowledgements[expectedMessageName] = {};\n }\n\n debugLogger.info(\n `Adding expected message ${expectedMessageName} from ${otherPeerIds.join(\n ', '\n )}.`\n );\n\n otherPeerIds.forEach((peerId) => {\n expectedMessageAcknowledgements[expectedMessageName][peerId] = {\n acknowledged: false,\n lastMessageSentAt: getTimeNow(),\n originalMessageName,\n originalData,\n shouldCancelMessageIfTimesOut,\n numberOfRetries: 0,\n maxNumberOfRetries: maxNumberOfRetries || defaultMaxRetries,\n messageRetryTime: messageRetryTime || defaultMessageRetryTime,\n };\n });\n };\n\n const getLastClockReceivedForInstanceOnScene = ({\n sceneNetworkId,\n instanceNetworkId,\n }: {\n sceneNetworkId: string;\n instanceNetworkId: string;\n }) => {\n if (!_lastClockReceivedByInstanceByScene[sceneNetworkId]) {\n _lastClockReceivedByInstanceByScene[sceneNetworkId] = {};\n }\n\n return (\n _lastClockReceivedByInstanceByScene[sceneNetworkId][\n instanceNetworkId\n ] || 0\n );\n };\n\n const setLastClockReceivedForInstanceOnScene = ({\n sceneNetworkId,\n instanceNetworkId,\n clock,\n }: {\n sceneNetworkId: string;\n instanceNetworkId: string;\n clock: number;\n }) => {\n if (!_lastClockReceivedByInstanceByScene[sceneNetworkId]) {\n _lastClockReceivedByInstanceByScene[sceneNetworkId] = {};\n }\n\n _lastClockReceivedByInstanceByScene[sceneNetworkId][\n instanceNetworkId\n ] = clock;\n };\n\n /**\n * Main function to send messages to other players, via P2P.\n * Takes into account the simulation of network latency and packet loss.\n */\n const sendDataTo = (\n peerIds: string[],\n messageName: string,\n data: object\n ): void => {\n if (\n SIMULATE_NETWORK_PACKET_LOSS_CHANCE > 0 &&\n Math.random() < SIMULATE_NETWORK_PACKET_LOSS_CHANCE\n ) {\n return;\n }\n\n if (\n SIMULATE_NETWORK_RANDOM_SLOW_PACKET_CHANCE > 0 &&\n Math.random() < SIMULATE_NETWORK_RANDOM_SLOW_PACKET_CHANCE\n ) {\n setTimeout(() => {\n gdjs.multiplayerPeerJsHelper.sendDataTo(peerIds, messageName, data);\n }, SIMULATE_NETWORK_RANDOM_LATENCY_MS);\n return;\n }\n\n if (SIMULATE_NETWORK_LATENCY_MS > 0) {\n setTimeout(() => {\n gdjs.multiplayerPeerJsHelper.sendDataTo(peerIds, messageName, data);\n }, SIMULATE_NETWORK_LATENCY_MS);\n return;\n }\n\n gdjs.multiplayerPeerJsHelper.sendDataTo(peerIds, messageName, data);\n };\n\n const findClosestInstanceWithoutNetworkId = (\n instances: gdjs.RuntimeObject[],\n x: number,\n y: number\n ): gdjs.RuntimeObject | null => {\n if (!instances.length) {\n // No instances, return null.\n return null;\n }\n\n // Avoid using a reduce function to avoid creating a new object at each iteration.\n let closestInstance: gdjs.RuntimeObject | null = null;\n let closestDistance = Infinity;\n for (let i = 0; i < instances.length; ++i) {\n if (instances[i].networkId) {\n // Skip instances that already have a network ID.\n continue;\n }\n\n const instance = instances[i];\n const distance =\n Math.pow(instance.getX() - x, 2) + Math.pow(instance.getY() - y, 2);\n if (distance < closestDistance) {\n closestInstance = instance;\n closestDistance = distance;\n }\n }\n\n return closestInstance;\n };\n\n const getInstanceFromNetworkId = ({\n runtimeScene,\n objectName,\n instanceNetworkId,\n instanceX,\n instanceY,\n shouldCreateIfNotFound,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n objectName: string;\n instanceNetworkId: string;\n instanceX?: number;\n instanceY?: number;\n shouldCreateIfNotFound?: boolean;\n }): gdjs.RuntimeObject | null => {\n const instances = runtimeScene.getInstancesOf(objectName);\n if (!instances) {\n // object does not exist in the scene, cannot find the instance.\n return null;\n }\n let instance =\n instances.find(\n (instance) => instance.networkId === instanceNetworkId\n ) || null;\n\n // If we know the position of the object, we can try to find the closest instance not synchronized yet.\n if (!instance && instanceX !== undefined && instanceY !== undefined) {\n debugLogger.info(\n `instance ${objectName} ${instanceNetworkId} not found with network ID, trying to find it with position ${instanceX}/${instanceY}.`\n );\n // Instance not found, it must be a new object.\n // 2 cases :\n // - The object was only created on the other player's game, so we create it and assign it the network ID.\n // - The object may have been created on all sides at the same time, so we try to find instances\n // of this object, that do not have a network ID yet, pick the one that is the closest to the\n // position of the object created by the other player, and assign it the network ID to start\n // synchronizing it.\n\n // Try to assign the network ID to the instance that is the closest to the position of the object created by the other player.\n const closestInstance = findClosestInstanceWithoutNetworkId(\n instances,\n instanceX,\n instanceY\n );\n\n if (closestInstance) {\n debugLogger.info(\n `Found closest instance for object ${objectName} ${instanceNetworkId} with no network ID.`\n );\n\n instance = closestInstance;\n instance.networkId = instanceNetworkId;\n }\n }\n\n // If we still did not find the instance, and we should create it if not found, then create it.\n if (!instance && shouldCreateIfNotFound) {\n debugLogger.info(\n `Instance ${instanceNetworkId} still not found, Creating instance ${objectName}.`\n );\n const newInstance = runtimeScene.createObject(objectName);\n if (!newInstance) {\n // Object does not exist in the scene, cannot create the instance.\n return null;\n }\n\n newInstance.networkId = instanceNetworkId;\n instance = newInstance;\n }\n\n return instance;\n };\n\n const changeInstanceOwnerMessageNamePrefix = '#changeInstanceOwner';\n const changeInstanceOwnerMessageNameRegex = /#changeInstanceOwner#owner_(\\d+)#object_(.+)#instance_(.+)/;\n const createChangeInstanceOwnerMessage = ({\n objectOwner,\n objectName,\n instanceNetworkId,\n newObjectOwner,\n instanceX,\n instanceY,\n sceneNetworkId,\n }: {\n objectOwner: number;\n objectName: string;\n instanceNetworkId: string;\n newObjectOwner: number;\n instanceX: number;\n instanceY: number;\n sceneNetworkId: string;\n }): {\n messageName: string;\n messageData: {\n previousOwner: number;\n newOwner: number;\n instanceX: number;\n instanceY: number;\n sceneNetworkId: string;\n };\n } => {\n return {\n messageName: `${changeInstanceOwnerMessageNamePrefix}#owner_${objectOwner}#object_${objectName}#instance_${instanceNetworkId}`,\n messageData: {\n previousOwner: objectOwner,\n newOwner: newObjectOwner,\n instanceX,\n instanceY,\n sceneNetworkId,\n },\n };\n };\n const instanceOwnerChangedMessageNamePrefix = '#instanceOwnerChanged';\n const instanceOwnerChangedMessageNameRegex = /#instanceOwnerChanged#owner_(\\d+)#object_(.+)#instance_(.+)/;\n const createInstanceOwnerChangedMessageNameFromChangeInstanceOwnerMessage = (\n messageName: string\n ): string => {\n return messageName.replace(\n changeInstanceOwnerMessageNamePrefix,\n instanceOwnerChangedMessageNamePrefix\n );\n };\n const handleChangeInstanceOwnerMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Change owner messages do not need to be saved for later use, as the game will automatically change the owner of\n // the instance when receiving an update message with a different owner.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n\n // When we receive ownership change messages, update the ownership of the instances in the scene.\n const instanceOwnershipChangeMessageNames = messageNamesArray.filter(\n (messageName) =>\n messageName.startsWith(changeInstanceOwnerMessageNamePrefix)\n );\n instanceOwnershipChangeMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const matches = changeInstanceOwnerMessageNameRegex.exec(messageName);\n if (!matches) {\n return;\n }\n const objectName = matches[2];\n const instanceNetworkId = matches[3];\n const previousOwner = messageData.previousOwner;\n const newOwner = messageData.newOwner;\n const sceneNetworkId = messageData.sceneNetworkId;\n\n if (sceneNetworkId !== runtimeScene.networkId) {\n debugLogger.info(\n `Object ${objectName} is in scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n // The object is not in the current scene.\n return;\n }\n\n const instance = getInstanceFromNetworkId({\n runtimeScene,\n objectName,\n instanceNetworkId,\n instanceX: messageData.instanceX,\n instanceY: messageData.instanceY,\n });\n\n if (!instance) {\n // Instance not found, it must have been destroyed already.\n debugLogger.info(\n `Instance ${instanceNetworkId} not found, it must have been destroyed.`\n );\n return;\n }\n\n const behavior = instance.getBehavior(\n 'MultiplayerObject'\n ) as MultiplayerObjectRuntimeBehavior | null;\n if (!behavior) {\n debugLogger.info(\n `Object ${objectName} does not have the MultiplayerObjectBehavior, cannot change ownership.`\n );\n return;\n }\n\n const currentPlayerObjectOwnership = behavior.getPlayerObjectOwnership();\n // Change is coherent if:\n const ownershipChangeIsCoherent =\n // the object is changing ownership from the same owner the host knew about,\n currentPlayerObjectOwnership === previousOwner ||\n // the object is already owned by the new owner. (may have been changed by another player faster)\n currentPlayerObjectOwnership === newOwner;\n if (\n gdjs.multiplayer.isCurrentPlayerHost() &&\n !ownershipChangeIsCoherent\n ) {\n // We received an ownership change message for an object which is in an unexpected state.\n // There may be some lag, and multiple ownership changes may have been sent by the other players.\n // As the host, let's not change the ownership and let the player revert it.\n debugLogger.info(\n `Object ${objectName} with instance network ID ${instanceNetworkId} does not have the expected owner. Wanted to change from ${previousOwner} to ${newOwner}, but object has owner ${currentPlayerObjectOwnership}.`\n );\n return;\n }\n\n // Force the ownership change.\n debugLogger.info(\n `Changing ownership of object ${objectName} to ${newOwner}.`\n );\n behavior.playerNumber = newOwner;\n\n const instanceOwnerChangedMessageName = createInstanceOwnerChangedMessageNameFromChangeInstanceOwnerMessage(\n messageName\n );\n\n debugLogger.info(\n `Sending acknowledgment of ownership change of object ${objectName} from ${previousOwner} to ${newOwner} with instance network ID ${instanceNetworkId} to ${messageSender}.`\n );\n // Once the instance ownership has changed, we need to acknowledge it to the player who sent this message.\n sendDataTo([messageSender], instanceOwnerChangedMessageName, {});\n\n // If we are the host,\n // so we need to relay the ownership change to others,\n // and expect an acknowledgment from them.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n // We don't need to send the message to the player who sent the ownership change message.\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n if (!otherPeerIds.length) {\n // No one else to relay the message to.\n return;\n }\n\n addExpectedMessageAcknowledgement({\n originalMessageName: messageName,\n originalData: messageData,\n expectedMessageName: instanceOwnerChangedMessageName,\n otherPeerIds,\n // As we are the host, we do not cancel the message if it times out.\n shouldCancelMessageIfTimesOut: false,\n });\n debugLogger.info(\n `Relaying ownership change of object ${objectName} with instance network ID ${instanceNetworkId} to ${otherPeerIds.join(\n ', '\n )}.`\n );\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const updateInstanceMessageNamePrefix = '#updateInstance';\n const updateInstanceMessageNameRegex = /#updateInstance#owner_(\\d+)#object_(.+)#instance_(.+)#scene_(.+)/;\n const createUpdateInstanceMessage = ({\n objectOwner,\n objectName,\n instanceNetworkId,\n objectNetworkSyncData,\n sceneNetworkId,\n }: {\n objectOwner: number;\n objectName: string;\n instanceNetworkId: string;\n objectNetworkSyncData: ObjectNetworkSyncData;\n sceneNetworkId: string;\n }): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: `${updateInstanceMessageNamePrefix}#owner_${objectOwner}#object_${objectName}#instance_${instanceNetworkId}#scene_${sceneNetworkId}`,\n messageData: objectNetworkSyncData,\n };\n };\n const handleUpdateInstanceMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Update instance messages do not need to be saved for later use, as the updates are sent pretty often,\n // a new one will be received very quickly.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n\n // When we receive update messages, update the instances in the scene.\n const objectUpdateMessageNames = messageNamesArray.filter((messageName) =>\n messageName.startsWith(updateInstanceMessageNamePrefix)\n );\n objectUpdateMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n\n // For object updates, we start from the newest message, as we want to apply the latest update,\n // and the old messages may have an outdated clock.\n // So we reverse the messages array.\n const reversedMessages = messages.slice().reverse();\n\n reversedMessages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const matches = updateInstanceMessageNameRegex.exec(messageName);\n if (!matches) {\n return;\n }\n const ownerPlayerNumber = parseInt(matches[1], 10);\n if (ownerPlayerNumber === gdjs.multiplayer.playerNumber) {\n // Do not update the instance if we receive an message from ourselves.\n // Should not happen but let's be safe.\n return;\n }\n const objectName = matches[2];\n const instanceNetworkId = matches[3];\n const sceneNetworkId = matches[4];\n\n if (sceneNetworkId !== runtimeScene.networkId) {\n debugLogger.info(\n `Object ${objectName} is in scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n // The object is not in the current scene.\n return;\n }\n\n const messageInstanceClock = messageData['_clock'];\n const lastClock = getLastClockReceivedForInstanceOnScene({\n sceneNetworkId,\n instanceNetworkId,\n });\n\n if (messageInstanceClock <= lastClock) {\n // Ignore old messages, they may be arriving out of order because of lag.\n return;\n }\n\n const instance = getInstanceFromNetworkId({\n runtimeScene,\n objectName,\n instanceNetworkId,\n // This can happen if the object was created on the other player's game, and we need to create it.\n shouldCreateIfNotFound: true,\n instanceX: messageData.x,\n instanceY: messageData.y,\n });\n if (!instance) {\n // This should not happen as we should have created the instance if it did not exist.\n logger.error('Instance could not be found or created.');\n return;\n }\n\n const behavior = instance.getBehavior(\n 'MultiplayerObject'\n ) as MultiplayerObjectRuntimeBehavior | null;\n if (!behavior) {\n logger.error(\n `Object ${objectName} does not have the MultiplayerObjectBehavior, cannot update it.`\n );\n // Object does not have the MultiplayerObjectBehavior, cannot update it.\n return;\n }\n\n // If we receive an update for this object for a different owner than the one we know about,\n // then 2 cases:\n // - If we are the owner of the object, then ignore the message, we assume it's a late update message or a wrong one,\n // we are confident that we own this object. (it may be reverted if we don't receive an acknowledgment in time)\n // - If we are not the owner of the object, then assume that we missed the ownership change message, so update the object's\n // ownership and then update the object.\n if (\n behavior.getPlayerObjectOwnership() ===\n gdjs.multiplayer.playerNumber\n ) {\n debugLogger.info(\n `Object ${objectName} with instance network ID ${instanceNetworkId} is owned by us ${gdjs.multiplayer.playerNumber}, ignoring update message from ${ownerPlayerNumber}.`\n );\n return;\n }\n\n if (behavior.getPlayerObjectOwnership() !== ownerPlayerNumber) {\n debugLogger.info(\n `Object ${objectName} with instance network ID ${instanceNetworkId} is owned by ${behavior.getPlayerObjectOwnership()} on our game, changing ownership to ${ownerPlayerNumber} as part of the update event.`\n );\n behavior.playerNumber = ownerPlayerNumber;\n }\n\n instance.updateFromNetworkSyncData(messageData);\n\n setLastClockReceivedForInstanceOnScene({\n sceneNetworkId,\n instanceNetworkId,\n clock: messageInstanceClock,\n });\n // Also update the clock on the behavior of this instance, so that if we take ownership of this object,\n // we can send the correct clock to the other players.\n behavior._clock = messageInstanceClock;\n\n // If we are are the host,\n // we need to relay the position to others except the player who sent the update message.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n if (!otherPeerIds.length) {\n // No one else to relay the message to.\n return;\n }\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const changeVariableOwnerMessageNamePrefix = '#changeVariableOwner';\n const changeVariableOwnerMessageNameRegex = /#changeVariableOwner#owner_(\\d+)#variable_(.+)/;\n const createChangeVariableOwnerMessage = ({\n variableOwner,\n variableNetworkId,\n newVariableOwner,\n }: {\n variableOwner: number;\n variableNetworkId: string;\n newVariableOwner: number;\n }): {\n messageName: string;\n messageData: {\n previousOwner: number;\n newOwner: number;\n };\n } => {\n return {\n messageName: `${changeVariableOwnerMessageNamePrefix}#owner_${variableOwner}#variable_${variableNetworkId}`,\n messageData: {\n previousOwner: variableOwner,\n newOwner: newVariableOwner,\n },\n };\n };\n const variableOwnerChangedMessageNamePrefix = '#variableOwnerChanged';\n const variableOwnerChangedMessageNameRegex = /#variableOwnerChanged#owner_(\\d+)#variable_(.+)/;\n const createVariableOwnerChangedMessageNameFromChangeVariableOwnerMessage = (\n messageName: string\n ): string => {\n return messageName.replace(\n changeVariableOwnerMessageNamePrefix,\n variableOwnerChangedMessageNamePrefix\n );\n };\n const handleChangeVariableOwnerMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Change owner messages do not need to be saved for later use, as the game will automatically change the owner of\n // the variable when receiving an update message with a different owner.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n\n // When we receive ownership change messages, find the variable and update its ownership.\n const variableOwnershipChangeMessageNames = messageNamesArray.filter(\n (messageName) =>\n messageName.startsWith(changeVariableOwnerMessageNamePrefix)\n );\n variableOwnershipChangeMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const matches = changeVariableOwnerMessageNameRegex.exec(messageName);\n if (!matches) {\n return;\n }\n const variableNetworkId = matches[2];\n const previousOwner = messageData.previousOwner;\n const newOwner = messageData.newOwner;\n\n const {\n type: variableType,\n name: variableName,\n containerId,\n } = gdjs.multiplayerVariablesManager.getVariableTypeAndNameFromNetworkId(\n variableNetworkId\n );\n\n // If this is a scene variable and we are not on the right scene, ignore it.\n if (\n variableType === 'scene' &&\n containerId !== runtimeScene.networkId\n ) {\n debugLogger.info(\n `Variable ${variableName} is in scene ${containerId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n // The variable is not in the current scene.\n return;\n }\n\n const variablesContainer =\n containerId === 'game'\n ? runtimeScene.getGame().getVariables()\n : runtimeScene.getVariables();\n\n if (!variablesContainer.has(variableName)) {\n // Variable not found, this should not happen.\n logger.error(\n `Variable with ID ${variableNetworkId} not found whilst syncing. This should not happen.`\n );\n return;\n }\n\n const variable = variablesContainer.get(variableName);\n\n const currentPlayerVariableOwnership = variable.getPlayerOwnership();\n // Change is coherent if:\n const ownershipChangeIsCoherent =\n // the variable is changing ownership from the same owner the host knew about,\n currentPlayerVariableOwnership === previousOwner ||\n // the variable is already owned by the new owner. (may have been changed by another player faster)\n currentPlayerVariableOwnership === newOwner;\n if (\n gdjs.multiplayer.isCurrentPlayerHost() &&\n !ownershipChangeIsCoherent\n ) {\n // We received an ownership change message for a variable which is in an unexpected state.\n // There may be some lag, and multiple ownership changes may have been sent by the other players.\n // As the host, let's not change the ownership and let the player revert it.\n debugLogger.info(\n `Variable with ID ${variableNetworkId} does not have the expected owner. Wanted to change from ${previousOwner} to ${newOwner}, but variable has owner ${currentPlayerVariableOwnership}.`\n );\n return;\n }\n\n // Force the ownership change.\n debugLogger.info(\n `Changing ownership of variable ${variableName} to ${newOwner}.`\n );\n variable.setPlayerOwnership(newOwner);\n\n const variableOwnerChangedMessageName = createVariableOwnerChangedMessageNameFromChangeVariableOwnerMessage(\n messageName\n );\n\n debugLogger.info(\n `Sending acknowledgment of ownership change of variable with ID ${variableNetworkId} from ${previousOwner} to ${newOwner} to ${messageSender}.`\n );\n // Once the variable ownership has changed, we need to acknowledge it to the player who sent this message.\n sendDataTo([messageSender], variableOwnerChangedMessageName, {});\n\n // If we are the host,\n // we need to relay the ownership change to others,\n // and expect an acknowledgment from them.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n // We don't need to send the message to the player who sent the ownership change message.\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n if (!otherPeerIds.length) {\n // No one else to relay the message to.\n return;\n }\n\n addExpectedMessageAcknowledgement({\n originalMessageName: messageName,\n originalData: messageData,\n expectedMessageName: variableOwnerChangedMessageName,\n otherPeerIds,\n // As we are the host, we do not cancel the message if it times out.\n shouldCancelMessageIfTimesOut: false,\n });\n debugLogger.info(\n `Relaying ownership change of variable with Id ${variableNetworkId} to ${otherPeerIds.join(\n ', '\n )}.`\n );\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const getRegexFromAckMessageName = (messageName: string) => {\n if (messageName.startsWith(instanceDestroyedMessageNamePrefix)) {\n return instanceDestroyedMessageNameRegex;\n } else if (\n messageName.startsWith(instanceOwnerChangedMessageNamePrefix)\n ) {\n return instanceOwnerChangedMessageNameRegex;\n } else if (\n messageName.startsWith(variableOwnerChangedMessageNamePrefix)\n ) {\n return variableOwnerChangedMessageNameRegex;\n } else if (messageName.startsWith(customMessageAcknowledgePrefix)) {\n return customMessageAcknowledgeRegex;\n }\n return null;\n };\n\n const isMessageAcknowledgement = (messageName: string) => {\n return (\n messageName.startsWith(instanceDestroyedMessageNamePrefix) ||\n messageName.startsWith(instanceOwnerChangedMessageNamePrefix) ||\n messageName.startsWith(variableOwnerChangedMessageNamePrefix) ||\n messageName.startsWith(customMessageAcknowledgePrefix)\n );\n };\n\n const handleAcknowledgeMessagesReceived = () => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Acknowledgment messages are mainly a response for ownership change, destruction, and custom messages,\n // which are not sent when the game is not ready.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n // When we receive acknowledgement messages, save it in the extension, to avoid sending the message again.\n const acknowledgedMessageNames = messageNamesArray.filter(\n isMessageAcknowledgement\n );\n acknowledgedMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n debugLogger.info(\n `Received acknowledgment for message ${messageName}.`\n );\n const regex = getRegexFromAckMessageName(messageName);\n if (!regex) {\n // This should not happen.\n logger.error(`Invalid acknowledgment message ${messageName}.`);\n return;\n }\n\n const matches = regex.exec(messageName);\n if (!matches) {\n // This should not happen.\n logger.error(`Invalid acknowledgment message ${messageName}.`);\n return;\n }\n if (!expectedMessageAcknowledgements[messageName]) {\n // This should not happen, but if we receive an acknowledgment for a message we did not expect, let's not error.\n return;\n }\n if (!expectedMessageAcknowledgements[messageName][messageSender]) {\n // This should not happen, but if we receive an acknowledgment from a sender we did not expect, let's not error.\n return;\n }\n\n // If a clock is provided in the message, ensure that we only process the message if the clock is newer than the last one received.\n const messageInstanceClock = messageData['_clock'];\n if (messageInstanceClock !== undefined) {\n const instanceNetworkId = matches[3];\n const sceneNetworkId = matches[4];\n const lastClock = getLastClockReceivedForInstanceOnScene({\n sceneNetworkId,\n instanceNetworkId,\n });\n if (messageInstanceClock <= lastClock) {\n // Ignore old messages.\n return;\n }\n\n setLastClockReceivedForInstanceOnScene({\n sceneNetworkId,\n instanceNetworkId,\n clock: messageInstanceClock,\n });\n }\n\n debugLogger.info(\n `Marking message ${messageName} as acknowledged from ${messageSender}.`\n );\n // Mark the acknowledgment as received.\n expectedMessageAcknowledgements[messageName][\n messageSender\n ].acknowledged = true;\n });\n });\n };\n\n const resendClearOrCancelAcknowledgedMessages = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Acknowledgment messages are mainly a response for ownership change, destruction, and custom messages,\n // which are not sent when the game is not ready.\n return;\n }\n\n // When all acknowledgments are received for an message, we can clear the message from our\n // list of expected acknowledgments.\n const expectedMessageNames = Object.keys(expectedMessageAcknowledgements);\n expectedMessageNames.forEach((acknowledgemessageName) => {\n const acknowledgements =\n expectedMessageAcknowledgements[acknowledgemessageName];\n const peerWhoHaventAcknowledged = Object.keys(acknowledgements).filter(\n (peerId) => !acknowledgements[peerId].acknowledged\n );\n if (!peerWhoHaventAcknowledged.length) {\n // All peers have acknowledged this message, we can clear the object.\n debugLogger.info(\n `All peers have acknowledged message ${acknowledgemessageName}.`\n );\n delete expectedMessageAcknowledgements[acknowledgemessageName];\n } else {\n // Some peers have not acknowledged the message, let's resend it to them.\n for (const peerId of peerWhoHaventAcknowledged) {\n const {\n lastMessageSentAt,\n originalMessageName,\n originalData,\n numberOfRetries: currentNumberOfRetries,\n maxNumberOfRetries,\n messageRetryTime,\n } = acknowledgements[peerId];\n if (getTimeNow() - lastMessageSentAt > messageRetryTime) {\n if (currentNumberOfRetries >= maxNumberOfRetries) {\n // We have retried too many times, let's give up.\n debugLogger.info(\n `Giving up on message ${acknowledgemessageName} for ${peerId}.`\n );\n if (acknowledgements[peerId].shouldCancelMessageIfTimesOut) {\n // If we should cancel the message if it times out, then revert it based on the original message.\n // INSTANCE OWNER CHANGE:\n if (\n originalMessageName.startsWith(\n changeInstanceOwnerMessageNamePrefix\n )\n ) {\n const matches = changeInstanceOwnerMessageNameRegex.exec(\n originalMessageName\n );\n if (!matches) {\n // This should not happen, if it does, remove the acknowledgment and return.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n const objectName = matches[2];\n const instanceNetworkId = matches[3];\n const instances = runtimeScene.getInstancesOf(objectName);\n if (!instances) {\n // object does not exist in the scene, cannot revert ownership.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n let instance = instances.find(\n (instance) => instance.networkId === instanceNetworkId\n );\n if (!instance) {\n // Instance not found, it must have been destroyed already, cannot revert ownership.\n // Should we recreate it?\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n const behavior = instance.getBehavior(\n 'MultiplayerObject'\n ) as MultiplayerObjectRuntimeBehavior | null;\n if (!behavior) {\n logger.error(\n `Object ${objectName} does not have the MultiplayerObjectBehavior, cannot revert ownership.`\n );\n // Object does not have the MultiplayerObjectBehavior, cannot revert ownership.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n const previousOwner = originalData.previousOwner;\n if (previousOwner === undefined) {\n // No previous owner, cannot revert ownership.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n // Force the ownership change.\n behavior.playerNumber = previousOwner || 0;\n }\n\n // VARIABLE OWNER CHANGE:\n if (\n originalMessageName.startsWith(\n changeVariableOwnerMessageNamePrefix\n )\n ) {\n const matches = changeVariableOwnerMessageNameRegex.exec(\n originalMessageName\n );\n if (!matches) {\n // This should not happen, if it does, remove the acknowledgment and return.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n const variableNetworkId = matches[2];\n const previousOwner = originalData.previousOwner;\n\n const {\n type: variableType,\n name: variableName,\n containerId,\n } = gdjs.multiplayerVariablesManager.getVariableTypeAndNameFromNetworkId(\n variableNetworkId\n );\n\n // If this is a scene variable and we are not on the right scene, ignore it.\n if (\n variableType === 'scene' &&\n containerId !== runtimeScene.networkId\n ) {\n debugLogger.info(\n `Variable ${variableName} is in scene ${containerId}, but we are on ${runtimeScene.networkId}. Skipping ownership revert.`\n );\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n const variablesContainer =\n containerId === 'game'\n ? runtimeScene.getGame().getVariables()\n : runtimeScene.getVariables();\n\n if (!variablesContainer.has(variableName)) {\n // Variable not found, this should not happen.\n logger.error(\n `Variable with ID ${variableNetworkId} not found while reverting ownership. This should not happen.`\n );\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n const variable = variablesContainer.get(variableName);\n\n if (previousOwner === undefined) {\n // No previous owner, cannot revert ownership.\n delete expectedMessageAcknowledgements[\n acknowledgemessageName\n ];\n return;\n }\n\n // Force the ownership change.\n variable.setPlayerOwnership(previousOwner || 0);\n }\n }\n delete expectedMessageAcknowledgements[acknowledgemessageName];\n continue;\n }\n\n // We have waited long enough for the acknowledgment, let's resend the message.\n sendDataTo([peerId], originalMessageName, originalData);\n // Reset the timestamp so that we wait again for the acknowledgment.\n acknowledgements[peerId].lastMessageSentAt = getTimeNow();\n // Increment the number of retries.\n acknowledgements[peerId].numberOfRetries =\n currentNumberOfRetries + 1;\n }\n }\n }\n });\n };\n\n const destroyInstanceMessageNamePrefix = '#destroyInstance';\n const destroyInstanceMessageNameRegex = /#destroyInstance#owner_(\\d+)#object_(.+)#instance_(.+)#scene_(.+)/;\n const createDestroyInstanceMessage = ({\n objectOwner,\n objectName,\n instanceNetworkId,\n sceneNetworkId,\n }: {\n objectOwner: number;\n objectName: string;\n instanceNetworkId: string;\n sceneNetworkId: string;\n }): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: `${destroyInstanceMessageNamePrefix}#owner_${objectOwner}#object_${objectName}#instance_${instanceNetworkId}#scene_${sceneNetworkId}`,\n messageData: {},\n };\n };\n const instanceDestroyedMessageNamePrefix = '#instanceDestroyed';\n const instanceDestroyedMessageNameRegex = /#instanceDestroyed#owner_(\\d+)#object_(.+)#instance_(.+)/;\n const createInstanceDestroyedMessageNameFromDestroyInstanceMessage = (\n messageName: string\n ): string => {\n return messageName.replace(\n destroyInstanceMessageNamePrefix,\n instanceDestroyedMessageNamePrefix\n );\n };\n const handleDestroyInstanceMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Destroy messages do not need to be saved for later use, as the game will automatically destroy\n // the instance if it does not receive an update message from it. So we return early.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n const destroyInstanceMessageNames = messageNamesArray.filter(\n (messageName) =>\n messageName.startsWith(destroyInstanceMessageNamePrefix)\n );\n destroyInstanceMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n debugLogger.info(\n `Received message ${messageName} with data ${JSON.stringify(\n messageData\n )}.`\n );\n const matches = destroyInstanceMessageNameRegex.exec(messageName);\n if (!matches) {\n return;\n }\n const playerNumber = parseInt(matches[1], 10);\n if (playerNumber === gdjs.multiplayer.playerNumber) {\n // Do not destroy the object if we receive an message from ourselves.\n // Should probably never happen.\n return;\n }\n const objectName = matches[2];\n const instanceNetworkId = matches[3];\n const sceneNetworkId = matches[4];\n\n if (sceneNetworkId !== runtimeScene.networkId) {\n // The object is not in the current scene.\n debugLogger.info(\n `Object ${objectName} is in scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n return;\n }\n\n const instance = getInstanceFromNetworkId({\n runtimeScene,\n objectName,\n instanceNetworkId,\n });\n\n const instanceDestroyedMessageName = createInstanceDestroyedMessageNameFromDestroyInstanceMessage(\n messageName\n );\n\n if (!instance) {\n debugLogger.info(\n 'Instance was not found in the scene, sending acknowledgment anyway.'\n );\n // Instance not found, it must have been destroyed already.\n // Send an acknowledgment to the player who sent the destroy message in case they missed it.\n sendDataTo([messageSender], instanceDestroyedMessageName, {});\n return;\n }\n\n debugLogger.info(\n `Destroying object ${objectName} with instance network ID ${instanceNetworkId}.`\n );\n instance.deleteFromScene(runtimeScene);\n\n debugLogger.info(\n `Sending acknowledgment of destruction of object ${objectName} with instance network ID ${instanceNetworkId} to ${messageSender}.`\n );\n // Once the object is destroyed, we need to acknowledge it to the player who sent the destroy message.\n sendDataTo([messageSender], instanceDestroyedMessageName, {});\n\n // If we are the host, we need to relay the destruction to others.\n // And expect an acknowledgment from everyone else as well.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n // We don't need to send the message to the player who sent the destroy message.\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n if (!otherPeerIds.length) {\n // No one else to relay the message to.\n return;\n }\n\n addExpectedMessageAcknowledgement({\n originalMessageName: messageName,\n originalData: messageData,\n expectedMessageName: instanceDestroyedMessageName,\n otherPeerIds,\n // As we are the host, we do not cancel the message if it times out.\n shouldCancelMessageIfTimesOut: false,\n });\n debugLogger.info(\n `Relaying instance destroyed message for object ${objectName} with instance network ID ${instanceNetworkId} to ${otherPeerIds.join(\n ', '\n )}.`\n );\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const customMessageNamePrefix = '#customMessage';\n const customMessageRegex = /#customMessage#(.+)/;\n const getCustomMessageNameFromUserMessageName = (\n userMessageName: string\n ) => {\n return `${customMessageNamePrefix}#${userMessageName}`;\n };\n const createCustomMessage = ({\n userMessageName,\n userMessageData,\n senderPlayerNumber,\n }: {\n userMessageName: string;\n userMessageData: any;\n senderPlayerNumber: number;\n }) => {\n const messageId = gdjs.makeUuid();\n return {\n messageName: getCustomMessageNameFromUserMessageName(userMessageName),\n messageData: {\n data: userMessageData,\n uniqueId: messageId,\n senderPlayerNumber, // We send the player number, so that other players who are not connected to us can know who sent the message.\n },\n };\n };\n const customMessageAcknowledgePrefix = '#ackCustomMessage';\n const customMessageAcknowledgeRegex = /#ackCustomMessage#(.+)/;\n const createAcknowledgeCustomMessageNameFromCustomMessage = (\n messageName: string\n ): string => {\n return messageName.replace(\n customMessageNamePrefix,\n customMessageAcknowledgePrefix\n );\n };\n\n const sendCustomMessage = (\n userMessageName: string,\n userMessageData: any // can be a simple string message or a serialized variable.\n ) => {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();\n const { messageName, messageData } = createCustomMessage({\n userMessageName,\n userMessageData,\n senderPlayerNumber: currentPlayerNumber,\n });\n const acknowledgmentMessageName = createAcknowledgeCustomMessageNameFromCustomMessage(\n messageName\n );\n addExpectedMessageAcknowledgement({\n originalMessageName: messageName,\n originalData: messageData,\n expectedMessageName: acknowledgmentMessageName,\n otherPeerIds: connectedPeerIds, // Expect acknowledgment from all peers.\n // custom messages cannot be reverted.\n shouldCancelMessageIfTimesOut: false,\n });\n debugLogger.info(\n `Sending custom message ${userMessageName} with data ${JSON.stringify(\n userMessageData\n )}.`\n );\n sendDataTo(connectedPeerIds, messageName, messageData);\n\n // If we are the host, we can consider this messaged as received\n // and add it to the list of custom messages to process on top of the messages received.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const messagesList = gdjs.multiplayerPeerJsHelper.getOrCreateMessagesList(\n messageName\n );\n messagesList.pushMessage(\n messageData,\n gdjs.multiplayerPeerJsHelper.getCurrentId()\n );\n // The message is now automatically added to the list of messages to process,\n // and will be removed at the end of the frame.\n }\n };\n\n const sendVariableCustomMessage = (\n userMessageName: string,\n variable: gdjs.Variable\n ) => {\n const userMessageData = variable.toJSObject();\n debugLogger.info(\n `Sending custom message ${userMessageName} with data ${JSON.stringify(\n userMessageData\n )}.`\n );\n sendCustomMessage(userMessageName, userMessageData);\n };\n\n const hasCustomMessageBeenReceived = (userMessageName: string) => {\n const customMessageName = getCustomMessageNameFromUserMessageName(\n userMessageName\n );\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messagesList = p2pMessagesMap.get(customMessageName);\n if (!messagesList) return; // No message received.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process.\n\n debugLogger.info(`custom message ${userMessageName} has been received.`);\n\n let customMessageHasNotAlreadyBeenProcessed = false;\n\n messages.forEach((message) => {\n const messageData = message.getData();\n const uniqueMessageId = messageData.uniqueId;\n const customMessageCacheKey = `${customMessageName}#${uniqueMessageId}`;\n if (processedCustomMessagesCache.has(customMessageCacheKey)) {\n // Message has already been processed recently. This can happen if the message is sent multiple times,\n // after not being acknowledged properly.\n return;\n }\n processedCustomMessagesCache.add(customMessageCacheKey);\n\n customMessageHasNotAlreadyBeenProcessed = true;\n return;\n });\n\n return customMessageHasNotAlreadyBeenProcessed;\n };\n\n const getCustomMessageData = (userMessageName: string) => {\n const customMessageName = getCustomMessageNameFromUserMessageName(\n userMessageName\n );\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messagesList = p2pMessagesMap.get(customMessageName);\n if (!messagesList) return; // No message received.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process.\n // Assume that the last message is the most recent one.\n const message = messages[messages.length - 1];\n\n const messageData = message.getData();\n return messageData.data;\n };\n\n const getVariableCustomMessageData = (\n userMessageName: string,\n variable: gdjs.Variable\n ) => {\n const data = getCustomMessageData(userMessageName);\n if (!data) {\n return;\n }\n debugLogger.info(\n `Received custom message ${userMessageName} with data ${JSON.stringify(\n data\n )}.`\n );\n variable.fromJSObject(data);\n };\n\n const getCustomMessageSender = (userMessageName: string): number => {\n const customMessageName = getCustomMessageNameFromUserMessageName(\n userMessageName\n );\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messagesList = p2pMessagesMap.get(customMessageName);\n if (!messagesList) return 0;\n const messages = messagesList.getMessages();\n if (!messages.length) return 0;\n // Assume that the last message is the most recent one.\n const message = messages[messages.length - 1];\n const messageData = message.getData();\n\n return messageData.senderPlayerNumber;\n };\n\n const handleCustomMessagesReceived = (): void => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Assume that the custom messages are not worth saving for later use.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n const customMessageNames = messageNamesArray.filter((messageName) =>\n messageName.startsWith(customMessageNamePrefix)\n );\n customMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) {\n logger.error(`No messages list found for ${messageName}.`);\n return; // Should not happen.\n }\n const messages = messagesList.getMessages();\n if (!messages.length) {\n return; // No messages to process for this name.\n }\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const uniqueMessageId = messageData.uniqueId;\n debugLogger.info(\n `Received custom message ${messageName} with data ${JSON.stringify(\n messageData\n )}.`\n );\n const matches = customMessageRegex.exec(messageName);\n if (!matches) {\n // This should not happen.\n logger.error(`Invalid custom message ${messageName}.`);\n return;\n }\n\n const customMessageCacheKey = `${messageName}#${uniqueMessageId}`;\n if (processedCustomMessagesCache.has(customMessageCacheKey)) {\n // Message has already been processed recently. This can happen if the message is sent multiple times,\n // after not being acknowledged properly.\n debugLogger.info(\n `Message ${messageName} has already been processed, skipping.`\n );\n return;\n }\n\n const acknowledgmentMessageName = createAcknowledgeCustomMessageNameFromCustomMessage(\n messageName\n );\n debugLogger.info(\n `Sending acknowledgment of custom message ${messageName} to ${messageSender}.`\n );\n sendDataTo([messageSender], acknowledgmentMessageName, {});\n\n // If we are the host,\n // so we need to relay the message to others.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n // In the case of custom messages, we relay the message to all players, including the sender.\n // This allows the sender to process it the same way others would, when they receive the event.\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n if (!connectedPeerIds.length) {\n // No one else to relay the message to.\n return;\n }\n\n addExpectedMessageAcknowledgement({\n originalMessageName: messageName,\n originalData: messageData,\n expectedMessageName: acknowledgmentMessageName,\n otherPeerIds: connectedPeerIds,\n // As we are the host, we do not cancel the message if it times out.\n shouldCancelMessageIfTimesOut: false,\n });\n sendDataTo(connectedPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const updateSceneMessageNamePrefix = '#updateScene';\n const createUpdateSceneMessage = ({\n sceneNetworkSyncData,\n }: {\n sceneNetworkSyncData: LayoutNetworkSyncData;\n }): {\n messageName: string;\n messageData: LayoutNetworkSyncData;\n } => {\n return {\n messageName: `${updateSceneMessageNamePrefix}`,\n messageData: sceneNetworkSyncData,\n };\n };\n\n const isSceneDifferentFromLastSync = (\n sceneSyncData: LayoutNetworkSyncData\n ) => {\n if (!sceneSyncData.var) {\n return false;\n }\n if (!lastSentSceneSyncData) {\n return true;\n }\n // Compare the json of the scene sync data to know if it has changed.\n // Not the most efficient way, but it's good enough for now.\n const haveVariableSyncDataChanged =\n JSON.stringify(sceneSyncData.var) !==\n JSON.stringify(lastSentSceneSyncData.var);\n\n return haveVariableSyncDataChanged;\n };\n\n const hasSceneBeenSyncedRecently = () => {\n return (\n getTimeNow() - lastSceneSyncTimestamp < 1000 / sceneSyncDataSyncRate\n );\n };\n\n const handleUpdateSceneMessagesToSend = (\n runtimeScene: gdjs.RuntimeScene\n ): void => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Don't send messages if the multiplayer is not ready.\n return;\n }\n\n const sceneNetworkSyncData = runtimeScene.getNetworkSyncData({\n playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),\n isHost: gdjs.multiplayer.isCurrentPlayerHost(),\n });\n if (!sceneNetworkSyncData) {\n return;\n }\n\n const isSceneSyncDataDifferent = isSceneDifferentFromLastSync(\n sceneNetworkSyncData\n );\n const shouldSyncScene =\n !hasSceneBeenSyncedRecently() ||\n isSceneSyncDataDifferent ||\n numberOfForcedSceneUpdates > 0;\n\n if (isSceneSyncDataDifferent) {\n numberOfForcedSceneUpdates = 3;\n }\n\n if (!shouldSyncScene) {\n return;\n }\n\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createUpdateSceneMessage({\n sceneNetworkSyncData,\n });\n\n sendDataTo(connectedPeerIds, messageName, messageData);\n\n lastSceneSyncTimestamp = getTimeNow();\n lastSentSceneSyncData = sceneNetworkSyncData;\n numberOfForcedSceneUpdates = Math.max(numberOfForcedSceneUpdates - 1, 0);\n };\n\n const handleUpdateSceneMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n const updateSceneMessageNames = messageNamesArray.filter((messageName) =>\n messageName.startsWith(updateSceneMessageNamePrefix)\n );\n updateSceneMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const sceneNetworkId = messageData.id;\n\n if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n if (sceneNetworkId !== runtimeScene.networkId) {\n debugLogger.info(\n `Received update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n // The scene is not the current scene.\n return;\n }\n\n runtimeScene.updateFromNetworkSyncData(messageData);\n } else {\n // If the game is not ready to receive game update messages, we need to save the data for later use.\n // This can happen when joining a game that is already running.\n debugLogger.info(\n `Saving scene ${sceneNetworkId} update message for later use.`\n );\n lastReceivedSceneSyncDataUpdates.store(messageData);\n return;\n }\n\n // If we are are the host,\n // we need to relay the scene update to others except the player who sent the update message.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n // We don't need to send the message to the player who sent the update message.\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const updateGameMessageNamePrefix = '#updateGame';\n const createUpdateGameMessage = ({\n gameNetworkSyncData,\n }: {\n gameNetworkSyncData: GameNetworkSyncData;\n }): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: `${updateGameMessageNamePrefix}`,\n messageData: gameNetworkSyncData,\n };\n };\n const isGameDifferentFromLastSync = (gameSyncData: GameNetworkSyncData) => {\n const variablesToSync = gameSyncData.var;\n const sceneStackToSync = gameSyncData.ss;\n if (!variablesToSync && !sceneStackToSync) {\n // Nothing to sync.\n return false;\n }\n\n if (\n !lastSentGameSyncData ||\n !lastSentGameSyncData.var ||\n !lastSentGameSyncData.ss\n ) {\n // We have not sent any game sync data yet, probably start of the game, let's do it.\n return true;\n }\n\n // Compare the json of the game variables sync data to know if it has changed.\n // Not the most efficient way, but it's good enough for now.\n if (\n variablesToSync &&\n JSON.stringify(variablesToSync) !==\n JSON.stringify(lastSentGameSyncData.var)\n ) {\n return true;\n }\n\n // For the sceneStack, loop through them one by one as it's more efficient.\n if (sceneStackToSync) {\n // If the length has changed, we're sure it's different.\n if (sceneStackToSync.length !== lastSentGameSyncData.ss.length) {\n return true;\n }\n\n for (let i = 0; i < sceneStackToSync.length; ++i) {\n const sceneToSync = sceneStackToSync[i];\n const lastSceneSent = lastSentGameSyncData.ss[i];\n if (\n sceneToSync.name !== lastSceneSent.name ||\n sceneToSync.networkId !== lastSceneSent.networkId\n ) {\n return true;\n }\n }\n }\n\n return false;\n };\n\n const hasGameBeenSyncedRecently = () => {\n return getTimeNow() - lastGameSyncTimestamp < 1000 / gameSyncDataSyncRate;\n };\n\n const handleUpdateGameMessagesToSend = (\n runtimeScene: gdjs.RuntimeScene\n ): void => {\n if (!gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n // Don't send messages if the multiplayer is not ready.\n return;\n }\n\n const gameNetworkSyncData = runtimeScene.getGame().getNetworkSyncData({\n playerNumber: gdjs.multiplayer.getCurrentPlayerNumber(),\n isHost: gdjs.multiplayer.isCurrentPlayerHost(),\n });\n if (!gameNetworkSyncData) {\n return;\n }\n\n const isGameSyncDataDifferent = isGameDifferentFromLastSync(\n gameNetworkSyncData\n );\n const shouldSyncGame =\n !hasGameBeenSyncedRecently() ||\n isGameSyncDataDifferent ||\n numberOfForcedGameUpdates > 0;\n\n if (isGameSyncDataDifferent) {\n numberOfForcedGameUpdates = 3;\n }\n\n if (!shouldSyncGame) {\n return;\n }\n\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createUpdateGameMessage({\n gameNetworkSyncData,\n });\n\n sendDataTo(connectedPeerIds, messageName, messageData);\n\n lastGameSyncTimestamp = getTimeNow();\n lastSentGameSyncData = gameNetworkSyncData;\n numberOfForcedGameUpdates = Math.max(numberOfForcedGameUpdates - 1, 0);\n };\n\n const handleUpdateGameMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n const updateGameMessageNames = messageNamesArray.filter((messageName) =>\n messageName.startsWith(updateGameMessageNamePrefix)\n );\n updateGameMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n if (gdjs.multiplayer.isReadyToSendOrReceiveGameUpdateMessages()) {\n runtimeScene.getGame().updateFromNetworkSyncData(messageData);\n } else {\n // If the game is not ready to receive game update messages, we need to save the data for later use.\n // This can happen when joining a game that is already running.\n debugLogger.info(`Saving game update message for later use.`);\n lastReceivedGameSyncDataUpdates.store(messageData);\n return;\n }\n\n // If we are are the host,\n // we need to relay the game update to others except the player who sent the update message.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n // We don't need to send the message to the player who sent the update message.\n const otherPeerIds = connectedPeerIds.filter(\n (peerId) => peerId !== messageSender\n );\n\n sendDataTo(otherPeerIds, messageName, messageData);\n }\n });\n });\n };\n\n const handleSavedUpdateMessages = (runtimeScene: gdjs.RuntimeScene) => {\n // Reapply the game saved updates.\n lastReceivedGameSyncDataUpdates.getUpdates().forEach((messageData) => {\n debugLogger.info(`Reapplying saved update of game.`);\n runtimeScene.getGame().updateFromNetworkSyncData(messageData);\n });\n // Game updates are always applied properly, so we can clear them.\n lastReceivedGameSyncDataUpdates.clear();\n\n // Then reapply the scene saved updates.\n lastReceivedSceneSyncDataUpdates.getUpdates().forEach((messageData) => {\n const sceneNetworkId = messageData.id;\n\n if (sceneNetworkId !== runtimeScene.networkId) {\n debugLogger.info(\n `Trying to apply saved update of scene ${sceneNetworkId}, but we are on ${runtimeScene.networkId}. Skipping.`\n );\n // The scene is not the current scene.\n return;\n }\n\n debugLogger.info(`Reapplying saved update of scene ${sceneNetworkId}.`);\n\n runtimeScene.updateFromNetworkSyncData(messageData);\n // We only remove the message if it was successfully applied, so it can be reapplied later,\n // in case we were not on the right scene.\n lastReceivedSceneSyncDataUpdates.remove(messageData);\n });\n };\n\n const heartbeatMessageNamePrefix = '#heartbeat';\n const heartbeastMessageRegex = /#heartbeat#(.+)/;\n const createHeartbeatMessage = (): {\n messageName: string;\n messageData: any;\n } => {\n // If we create the heartbeat meassage, we are the host,\n // Ensure our player number is correctly set when the first heartbeat is sent.\n _playersInfo[gdjs.multiplayer.getCurrentPlayerNumber()] = {\n ping: 0, // we are the host, so we don't need to compute the ping.\n playerId: gdjs.playerAuthentication.getUserId(),\n username: gdjs.playerAuthentication.getUsername(),\n };\n for (const playerNumber in _playersInfo) {\n _playersInfo[playerNumber] = {\n ..._playersInfo[playerNumber],\n ping: getPlayerPing(parseInt(playerNumber, 10)),\n };\n }\n return {\n messageName: `${heartbeatMessageNamePrefix}#${gdjs.multiplayer.getCurrentPlayerNumber()}`,\n messageData: {\n now: getTimeNow(), // we send the current time to compute the ping.\n playersInfo: _playersInfo,\n },\n };\n };\n const createHeartbeatAnswerMessage = ({\n heartbeatSentAt,\n }: {\n heartbeatSentAt: number;\n }): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: `${heartbeatMessageNamePrefix}#${gdjs.multiplayer.getCurrentPlayerNumber()}`,\n messageData: {\n sentAt: heartbeatSentAt,\n playerId: gdjs.playerAuthentication.getUserId(),\n username: gdjs.playerAuthentication.getUsername(),\n },\n };\n };\n const hasSentHeartbeatRecently = () => {\n return (\n !!lastHeartbeatSentTimestamp &&\n getTimeNow() - lastHeartbeatSentTimestamp < 1000 / heartbeatSyncRate\n );\n };\n const handleHeartbeatsToSend = () => {\n // Only host sends heartbeats to all players regularly:\n // - it allows them to send a heartbeat back immediately so that the host can compute the ping.\n // - it allows to pass along the pings of all players to all players.\n if (!gdjs.multiplayer.isCurrentPlayerHost()) {\n return;\n }\n\n const shouldSendHeartbeat = !hasSentHeartbeatRecently();\n if (!shouldSendHeartbeat) {\n return;\n }\n\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createHeartbeatMessage();\n sendDataTo(connectedPeerIds, messageName, messageData);\n\n lastHeartbeatSentTimestamp = getTimeNow();\n };\n\n const handleHeartbeatsReceived = () => {\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const messageNamesArray = Array.from(p2pMessagesMap.keys());\n const heartbeatMessageNames = messageNamesArray.filter((messageName) =>\n messageName.startsWith(heartbeatMessageNamePrefix)\n );\n heartbeatMessageNames.forEach((messageName) => {\n const messagesList = p2pMessagesMap.get(messageName);\n if (!messagesList) return; // Should not happen.\n const messages = messagesList.getMessages();\n if (!messages.length) return; // No messages to process for this name.\n messages.forEach((message) => {\n const messageData = message.getData();\n const messageSender = message.getSender();\n const matches = heartbeastMessageRegex.exec(messageName);\n if (!matches) {\n return;\n }\n const playerNumber = parseInt(matches[1], 10);\n // Ensure we know who is who.\n _peerIdToPlayerNumber[messageSender] = playerNumber;\n\n // If we are not the host, save what the host told us about the other players info\n // and respond with a heartbeat immediately, informing the host of our playerId and username.\n if (!gdjs.multiplayer.isCurrentPlayerHost()) {\n const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();\n const currentlyKnownPlayerNumbers = Object.keys(\n _playersInfo\n ).map((playerNumber) => parseInt(playerNumber, 10));\n const receivedPlayerNumbers = Object.keys(\n messageData.playersInfo\n ).map((playerNumber) => parseInt(playerNumber, 10));\n const currentlyKnownPingForCurrentUser =\n _playersInfo[currentPlayerNumber] &&\n _playersInfo[currentPlayerNumber].ping;\n // If there are no players info yet, we're probably just connecting.\n // This can happen when joining a game that is already running.\n // Do not handle this case to avoid displaying too many notifications.\n if (!!currentlyKnownPlayerNumbers.length) {\n // Look at the players info received to know if there are new players who just connected.\n const newPlayerNumbers = receivedPlayerNumbers.filter(\n (playerNumber) =>\n !currentlyKnownPlayerNumbers.includes(playerNumber) &&\n playerNumber !== currentPlayerNumber // Do not consider ourselves as a new player.\n );\n _playerNumbersWhoJustJoined.push(...newPlayerNumbers);\n // Or players who have disconnected.\n const playerNumbersWhoHaveDisconnected = currentlyKnownPlayerNumbers.filter(\n (playerNumber) => !receivedPlayerNumbers.includes(playerNumber)\n );\n _playerNumbersWhoJustLeft.push(\n ...playerNumbersWhoHaveDisconnected\n );\n for (const playerNumber of playerNumbersWhoHaveDisconnected) {\n // Temporarily save the username in another variable to be used for the notification,\n // as we're deleting its playerInfo just after.\n _temporaryPlayerNumberToUsername[\n playerNumber\n ] = getPlayerUsername(playerNumber);\n }\n }\n\n // Save the players info received from the host.\n // Avoid overwriting the whole object as it can mess up tests that rely on the object reference.\n cloneObjectWithoutOverwriting({\n source: messageData.playersInfo,\n target: _playersInfo,\n });\n\n const {\n messageName: answerMessageName,\n messageData: answerMessageData,\n } = createHeartbeatAnswerMessage({\n heartbeatSentAt: messageData.now, // We send back the time we received, so that the host can compute the ping.\n });\n sendDataTo([messageSender], answerMessageName, answerMessageData);\n // We have received a heartbeat from the host, informing us of our ping,\n // so we can consider the connection as working.\n if (\n _playersInfo[currentPlayerNumber] !== undefined &&\n _playersInfo[currentPlayerNumber].ping !== undefined\n ) {\n gdjs.multiplayer.markConnectionAsConnected();\n if (currentlyKnownPingForCurrentUser === undefined) {\n // We just connected, let's add ourselves to the list of players who just connected,\n // for the notification and the events.\n _playerNumbersWhoJustJoined.push(currentPlayerNumber);\n }\n }\n\n return;\n }\n\n // If we are the host.\n\n // If this is a new player, we're about to send them their ping, so we can consider them connected.\n if (!_playersInfo[playerNumber]) {\n _playerNumbersWhoJustJoined.push(playerNumber);\n }\n\n // compute the pings based on:\n // - the time we received the heartbeat.\n // - the time the heartbeat was sent.\n const now = getTimeNow();\n const heartbeatSentAt = messageData.sentAt;\n const roundTripTime = Math.round(now - heartbeatSentAt);\n const playerLastRoundTripTimes =\n _playersLastRoundTripTimes[playerNumber] || [];\n playerLastRoundTripTimes.push(roundTripTime);\n if (playerLastRoundTripTimes.length > 5) {\n // Keep only the last 5 RTT to compute the average.\n playerLastRoundTripTimes.shift();\n }\n _playersLastRoundTripTimes[playerNumber] = playerLastRoundTripTimes;\n\n let sum = 0;\n for (const lastRoundTripTime of playerLastRoundTripTimes) {\n sum += lastRoundTripTime;\n }\n const averagePing = Math.round(\n sum / playerLastRoundTripTimes.length / 2 // Divide by 2 to get the one way ping.\n );\n _playersInfo[playerNumber] = {\n ping: averagePing,\n playerId: messageData.playerId,\n username: messageData.username,\n };\n\n // If there are new players, let's resend a heartbeat right away so that everyone is aware of them\n // on approximately the same frame.\n if (_playerNumbersWhoJustJoined.length) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createHeartbeatMessage();\n sendDataTo(connectedPeerIds, messageName, messageData);\n lastHeartbeatSentTimestamp = getTimeNow();\n }\n });\n });\n };\n\n const hasReceivedHeartbeatFromPlayer = (playerNumber: number) => {\n // Consider that a player has sent a heartbeat if we have been able to calculate\n // at least one round trip time for them.\n const playerLastRoundTripTimes =\n _playersLastRoundTripTimes[playerNumber] || [];\n return playerLastRoundTripTimes.length > 0;\n };\n\n const getPlayerPing = (playerNumber: number) => {\n const playerInfo = _playersInfo[playerNumber];\n if (!playerInfo) {\n return 0;\n }\n return playerInfo.ping || 0;\n };\n\n const getCurrentPlayerPing = () => {\n const currentPlayerNumber = gdjs.multiplayer.getCurrentPlayerNumber();\n return getPlayerPing(currentPlayerNumber);\n };\n\n const markPlayerAsDisconnected = ({\n runtimeScene,\n playerNumber,\n peerId,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n playerNumber: number;\n peerId?: string;\n }) => {\n logger.info(`Marking player ${playerNumber} as disconnected.`);\n _playerNumbersWhoJustLeft.push(playerNumber);\n // Temporarily save the username in another variable to be used for the notification,\n // as we're deleting its playerInfo just after.\n _temporaryPlayerNumberToUsername[playerNumber] = getPlayerUsername(\n playerNumber\n );\n clearPlayerTempData(playerNumber);\n\n // If Host has disconnected, either switch host or stop the game.\n if (peerId && peerId === gdjs.multiplayer.hostPeerId) {\n const shouldEndLobbyGame = gdjs.multiplayer.shouldEndLobbyWhenHostLeaves();\n if (shouldEndLobbyGame) {\n logger.info('Host has disconnected, ending the game.');\n\n clearAllMessagesTempData();\n gdjs.multiplayer.handleLobbyGameEnded();\n } else {\n logger.info('Host has disconnected, switching host.');\n\n gdjs.multiplayer.handleHostDisconnected({ runtimeScene });\n return;\n }\n }\n\n // If we are the host, send a heartbeat right away so that everyone is aware of the disconnection\n // on approximately the same frame.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createHeartbeatMessage();\n sendDataTo(connectedPeerIds, messageName, messageData);\n lastHeartbeatSentTimestamp = getTimeNow();\n }\n };\n\n const getPlayerUsername = (playerNumber: number) => {\n return (\n (_playersInfo[playerNumber] || {}).username ||\n _temporaryPlayerNumberToUsername[playerNumber] ||\n `Player ${playerNumber}`\n );\n };\n\n const getPlayerId = (playerNumber: number) => {\n return (_playersInfo[playerNumber] || {}).playerId || '';\n };\n\n const handleJustDisconnectedPeers = (runtimeScene: RuntimeScene) => {\n // If the game is not running, we don't need to handle disconnected peers.\n if (!gdjs.multiplayer.isLobbyGameRunning()) {\n return;\n }\n\n // We rely on the p2p helper to know who has disconnected.\n const justDisconnectedPlayers: {\n playerNumber: number;\n peerId: string;\n }[] = [];\n\n const justDisconnectedPeers = gdjs.multiplayerPeerJsHelper.getJustDisconnectedPeers();\n if (justDisconnectedPeers.length) {\n for (const disconnectedPeer of justDisconnectedPeers) {\n const disconnectedPlayerNumber =\n _peerIdToPlayerNumber[disconnectedPeer];\n if (!disconnectedPlayerNumber) {\n // This should not happen.\n return;\n }\n logger.info(`Player ${disconnectedPlayerNumber} has disconnected.`);\n justDisconnectedPlayers.push({\n playerNumber: disconnectedPlayerNumber,\n peerId: disconnectedPeer,\n });\n }\n }\n\n for (const { playerNumber, peerId } of justDisconnectedPlayers) {\n // When a player disconnects, as the host, we look at all the instances\n // they own and decide what to do with them.\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n const instances = runtimeScene.getAdhocListOfAllInstances();\n for (const instance of instances) {\n const behavior = instance.getBehavior(\n 'MultiplayerObject'\n ) as MultiplayerObjectRuntimeBehavior | null;\n if (\n behavior &&\n behavior.getPlayerObjectOwnership() === playerNumber\n ) {\n const actionOnPlayerDisconnect = behavior.getActionOnPlayerDisconnect();\n if (actionOnPlayerDisconnect === 'DestroyObject') {\n // No need to remove the ownership, as the destroy message will be sent to all players.\n instance.deleteFromScene(runtimeScene);\n } else if (actionOnPlayerDisconnect === 'GiveOwnershipToHost') {\n // Removing the ownership will send a message to all players.\n behavior.removeObjectOwnership();\n } else if (actionOnPlayerDisconnect === 'DoNothing') {\n // Do nothing.\n }\n }\n }\n }\n\n markPlayerAsDisconnected({ runtimeScene, playerNumber, peerId });\n }\n };\n\n const hasAnyPlayerJustLeft = (): boolean => {\n return _playerNumbersWhoJustLeft.length > 0;\n };\n const hasPlayerJustLeft = (playerNumber: number): boolean => {\n return _playerNumbersWhoJustLeft.includes(playerNumber);\n };\n const getPlayersWhoJustLeft = (): number[] => {\n return _playerNumbersWhoJustLeft;\n };\n const getLatestPlayerWhoJustLeft = (): number => {\n return _playerNumbersWhoJustLeft[0] || 0;\n };\n const removePlayerWhoJustLeft = (): void => {\n // Avoid using shift for test purposes, as it modifies the reference.\n const playerNumberWhoLeft = _playerNumbersWhoJustLeft[0];\n if (playerNumberWhoLeft !== undefined) {\n _playerNumbersWhoJustLeft = _playerNumbersWhoJustLeft.slice(1);\n delete _temporaryPlayerNumberToUsername[playerNumberWhoLeft];\n }\n };\n\n const hasAnyPlayerJustJoined = () => {\n return _playerNumbersWhoJustJoined.length > 0;\n };\n const hasPlayerJustJoined = (playerNumber: number): boolean => {\n return _playerNumbersWhoJustJoined.includes(playerNumber);\n };\n const getPlayersWhoJustJoined = () => {\n return _playerNumbersWhoJustJoined;\n };\n const getLatestPlayerWhoJustJoined = (): number => {\n return _playerNumbersWhoJustJoined[0] || 0;\n };\n const removePlayerWhoJustJoined = (): void => {\n // Avoid using shift for test purposes, as it modifies the reference.\n const playerNumberWhoJoined = _playerNumbersWhoJustJoined[0];\n if (playerNumberWhoJoined !== undefined) {\n _playerNumbersWhoJustJoined = _playerNumbersWhoJustJoined.slice(1);\n }\n };\n\n const getConnectedPlayers = () => {\n return Object.keys(_playersInfo).map((playerNumber) => ({\n playerNumber: parseInt(playerNumber, 10),\n playerId: _playersInfo[playerNumber].playerId,\n }));\n };\n const getNumberOfConnectedPlayers = () => {\n // Look at the player info as a way to know how many players are in the lobby.\n // This object is updated when heartbeats are sent and received.\n return Object.keys(_playersInfo).length;\n };\n const isPlayerConnected = (playerNumber: number) => {\n return _playersInfo[playerNumber] !== undefined;\n };\n\n const getPlayersInfo = () => {\n return _playersInfo;\n };\n\n const endGameMessageName = '#endGame';\n const createEndGameMessage = (): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: endGameMessageName,\n messageData: {},\n };\n };\n const sendEndGameMessage = () => {\n // Only the host can end the game.\n if (!gdjs.multiplayer.isCurrentPlayerHost()) {\n return;\n }\n\n debugLogger.info(`Sending endgame message.`);\n\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createEndGameMessage();\n // Note: we don't wait for an acknowledgment here, as the game will end anyway.\n sendDataTo(connectedPeerIds, messageName, messageData);\n };\n\n const handleEndGameMessagesReceived = () => {\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n // Only other players need to react to the end game message.\n return;\n }\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const endGameMessagesList = p2pMessagesMap.get(endGameMessageName);\n if (!endGameMessagesList) {\n return; // No end game message received.\n }\n const messages = endGameMessagesList.getMessages();\n if (!messages.length) return; // No messages to process.\n\n logger.info(`Received endgame message.`);\n\n // If the message is received more than 1 time, we just ignore it and end the game.\n\n clearAllMessagesTempData();\n gdjs.multiplayer.handleLobbyGameEnded();\n };\n\n const resumeGameMessageName = '#resumeGame';\n const createResumeGameMessage = (): {\n messageName: string;\n messageData: any;\n } => {\n return {\n messageName: resumeGameMessageName,\n messageData: {},\n };\n };\n const sendResumeGameMessage = () => {\n // Only the host can inform others that the game is resuming.\n if (!gdjs.multiplayer.isCurrentPlayerHost()) {\n return;\n }\n\n debugLogger.info(`Sending resumeGame message.`);\n\n const connectedPeerIds = gdjs.multiplayerPeerJsHelper.getAllPeers();\n const { messageName, messageData } = createResumeGameMessage();\n sendDataTo(connectedPeerIds, messageName, messageData);\n };\n\n const handleResumeGameMessagesReceived = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (gdjs.multiplayer.isCurrentPlayerHost()) {\n // Only other players need to react to resume game message.\n return;\n }\n\n const p2pMessagesMap = gdjs.multiplayerPeerJsHelper.getAllMessagesMap();\n const resumeGameMessagesList = p2pMessagesMap.get(resumeGameMessageName);\n if (!resumeGameMessagesList) {\n return; // No resume game message received.\n }\n const messages = resumeGameMessagesList.getMessages();\n if (!messages.length) return; // No messages to process.\n\n logger.info(`Received resumeGame message.`);\n\n gdjs.multiplayer.resumeGame(runtimeScene);\n };\n\n const clearAllMessagesTempData = () => {\n _playersLastRoundTripTimes = {};\n _playersInfo = {};\n lastReceivedGameSyncDataUpdates.clear();\n lastReceivedSceneSyncDataUpdates.clear();\n processedCustomMessagesCache.clear();\n _playerNumbersWhoJustLeft = [];\n _playerNumbersWhoJustJoined = [];\n expectedMessageAcknowledgements = {};\n _lastClockReceivedByInstanceByScene = {};\n };\n\n const clearPlayerTempData = (playerNumber: number) => {\n // Remove the player from the list of players.\n // This will cause the next hearbeat to not include this player\n // and the others will consider them as disconnected.\n delete _playersLastRoundTripTimes[playerNumber];\n delete _playersInfo[playerNumber];\n };\n\n return {\n sendDataTo,\n // Acks.\n addExpectedMessageAcknowledgement,\n handleAcknowledgeMessagesReceived,\n resendClearOrCancelAcknowledgedMessages,\n // Instance ownership.\n createChangeInstanceOwnerMessage,\n createInstanceOwnerChangedMessageNameFromChangeInstanceOwnerMessage,\n handleChangeInstanceOwnerMessagesReceived,\n // Instance update.\n createUpdateInstanceMessage,\n handleUpdateInstanceMessagesReceived,\n // Instance destruction.\n createDestroyInstanceMessage,\n createInstanceDestroyedMessageNameFromDestroyInstanceMessage,\n handleDestroyInstanceMessagesReceived,\n // Variable ownership.\n createChangeVariableOwnerMessage,\n createVariableOwnerChangedMessageNameFromChangeVariableOwnerMessage,\n handleChangeVariableOwnerMessagesReceived,\n // Custom messages.\n sendCustomMessage,\n getCustomMessageData,\n sendVariableCustomMessage,\n getVariableCustomMessageData,\n hasCustomMessageBeenReceived,\n handleCustomMessagesReceived,\n getCustomMessageSender,\n // Scene update.\n createUpdateSceneMessage,\n handleUpdateSceneMessagesToSend,\n handleUpdateSceneMessagesReceived,\n // Game update.\n createUpdateGameMessage,\n handleUpdateGameMessagesToSend,\n handleUpdateGameMessagesReceived,\n handleSavedUpdateMessages,\n // Heartbeats.\n handleHeartbeatsToSend,\n handleHeartbeatsReceived,\n hasReceivedHeartbeatFromPlayer,\n // Pings & usernames.\n getPlayerPing,\n getCurrentPlayerPing,\n getPlayerUsername,\n getPlayerId,\n // Connected players.\n handleJustDisconnectedPeers,\n getConnectedPlayers,\n getNumberOfConnectedPlayers,\n isPlayerConnected,\n getPlayersInfo,\n // Leaving players.\n hasAnyPlayerJustLeft,\n hasPlayerJustLeft,\n getPlayersWhoJustLeft,\n getLatestPlayerWhoJustLeft,\n removePlayerWhoJustLeft,\n markPlayerAsDisconnected,\n // Joining players.\n hasAnyPlayerJustJoined,\n hasPlayerJustJoined,\n getPlayersWhoJustJoined,\n getLatestPlayerWhoJustJoined,\n removePlayerWhoJustJoined,\n // End game.\n sendEndGameMessage,\n handleEndGameMessagesReceived,\n clearAllMessagesTempData,\n // Resume game after migration.\n sendResumeGameMessage,\n handleResumeGameMessagesReceived,\n };\n };\n\n /**\n * The MultiplayerMessageManager used by the game.\n */\n export let multiplayerMessageManager = makeMultiplayerMessageManager();\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|