gdcore-tools 2.0.0-beta5 → 2.0.0-beta6
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/CustomRuntimeObject.js +1 -1
- package/dist/Runtime/CustomRuntimeObject.js.map +2 -2
- package/dist/Runtime/CustomRuntimeObjectInstanceContainer.js +1 -1
- package/dist/Runtime/CustomRuntimeObjectInstanceContainer.js.map +2 -2
- package/dist/Runtime/Extensions/3D/CustomRuntimeObject3D.js +1 -1
- package/dist/Runtime/Extensions/3D/CustomRuntimeObject3D.js.map +2 -2
- package/dist/Runtime/Extensions/AdMob/admobtools.js +1 -1
- package/dist/Runtime/Extensions/AdMob/admobtools.js.map +2 -2
- package/dist/Runtime/Extensions/AnchorBehavior/anchorruntimebehavior.js +1 -1
- package/dist/Runtime/Extensions/AnchorBehavior/anchorruntimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/JsExtension.js +2 -2
- package/dist/Runtime/Extensions/Multiplayer/JsExtension.js +97 -7
- package/dist/Runtime/Extensions/Multiplayer/messageManager.js +1 -1
- package/dist/Runtime/Extensions/Multiplayer/messageManager.js.map +2 -2
- package/dist/Runtime/Extensions/Multiplayer/multiplayercomponents.js +1 -1
- package/dist/Runtime/Extensions/Multiplayer/multiplayercomponents.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/Multiplayer/multiplayertools.js +1 -1
- package/dist/Runtime/Extensions/Multiplayer/multiplayertools.js.map +2 -2
- package/dist/Runtime/Extensions/PanelSpriteObject/panelspriteruntimeobject-pixi-renderer.js +1 -1
- package/dist/Runtime/Extensions/PanelSpriteObject/panelspriteruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/Physics2Behavior/JsExtension.js +62 -27
- package/dist/Runtime/Extensions/Physics2Behavior/physics2runtimebehavior.js +1 -1
- package/dist/Runtime/Extensions/Physics2Behavior/physics2runtimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/Spine/JsExtension.js +32 -0
- package/dist/Runtime/Extensions/Spine/spineruntimeobject-pixi-renderer.js +1 -1
- package/dist/Runtime/Extensions/Spine/spineruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/Spine/spineruntimeobject.js +1 -1
- package/dist/Runtime/Extensions/Spine/spineruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/TextInput/JsExtension.js +3 -1
- package/dist/Runtime/Extensions/TileMap/JsExtension.js +84 -29
- package/dist/Runtime/Extensions/TileMap/collision/TransformedTileMap.js +1 -1
- package/dist/Runtime/Extensions/TileMap/collision/TransformedTileMap.js.map +2 -2
- package/dist/Runtime/Extensions/TileMap/helper/TileMapHelper.js +1 -1
- package/dist/Runtime/Extensions/TileMap/helper/TileMapHelper.js.map +1 -1
- package/dist/Runtime/Extensions/TileMap/helper/dts/load/tiled/TiledTileMapLoader.d.ts.map +1 -1
- package/dist/Runtime/Extensions/TileMap/helper/dts/model/TileMapModel.d.ts +12 -1
- package/dist/Runtime/Extensions/TileMap/helper/dts/model/TileMapModel.d.ts.map +1 -1
- package/dist/Runtime/Extensions/TileMap/helper/dts/render/TileMapManager.d.ts.map +1 -1
- package/dist/Runtime/Extensions/TileMap/simpletilemapruntimeobject.js +1 -1
- package/dist/Runtime/Extensions/TileMap/simpletilemapruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/TileMap/tilemapruntimeobject-pixi-renderer.js +1 -1
- package/dist/Runtime/Extensions/TileMap/tilemapruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/TweenBehavior/JsExtension.js +1 -0
- package/dist/Runtime/RuntimeInstanceContainer.js.map +2 -2
- package/dist/Runtime/SpriteAnimator.js +1 -1
- package/dist/Runtime/SpriteAnimator.js.map +2 -2
- package/dist/Runtime/debugger-client/InGameDebugger.js +1 -1
- package/dist/Runtime/debugger-client/InGameDebugger.js.map +2 -2
- package/dist/Runtime/debugger-client/abstract-debugger-client.js +1 -1
- package/dist/Runtime/debugger-client/abstract-debugger-client.js.map +2 -2
- package/dist/Runtime/debugger-client/hot-reloader.js +2 -1
- package/dist/Runtime/debugger-client/hot-reloader.js.map +2 -2
- package/dist/Runtime/debugger-client/minimal-debugger-client.js +2 -0
- package/dist/Runtime/debugger-client/minimal-debugger-client.js.map +7 -0
- package/dist/Runtime/debugger-client/websocket-debugger-client.js.map +2 -2
- package/dist/Runtime/debugger-client/window-message-debugger-client.js.map +2 -2
- package/dist/Runtime/pixi-renderers/spriteruntimeobject-pixi-renderer.js +1 -1
- package/dist/Runtime/pixi-renderers/spriteruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/runtimegame.js +1 -1
- package/dist/Runtime/runtimegame.js.map +2 -2
- package/dist/Runtime/runtimescene.js +1 -1
- package/dist/Runtime/runtimescene.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/project-data.d.ts +31 -8
- package/dist/Runtime/variablescontainer.js +1 -1
- package/dist/Runtime/variablescontainer.js.map +2 -2
- package/dist/lib/libGD.cjs +1 -1
- package/dist/lib/libGD.wasm +0 -0
- package/gd.d.ts +77 -47
- package/package.json +2 -2
- package/dist/lib/libGD.d.cts +0 -5
- package/dist/loaders.d.cts +0 -2
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../GDevelop/Extensions/Multiplayer/multiplayertools.ts"],
|
|
4
|
-
"sourcesContent": ["namespace gdjs {\n const logger = new gdjs.Logger('Multiplayer');\n\n export namespace multiplayer {\n /** Set to true in testing to avoid relying on the multiplayer extension. */\n export let disableMultiplayerForTesting = false;\n\n export let _isReadyToSendOrReceiveGameUpdateMessages = false;\n\n let _isGameRegistered: boolean | null = null;\n let _isCheckingIfGameIsRegistered = false;\n let _isWaitingForLogin = false;\n\n let _hasLobbyGameJustStarted = false;\n export let _isLobbyGameRunning = false;\n let _hasLobbyGameJustEnded = false;\n let _lobbyId: string | null = null;\n let _connectionId: string | null = null;\n\n // Communication methods.\n let _lobbiesMessageCallback: ((event: MessageEvent) => void) | null = null;\n let _websocket: WebSocket | null = null;\n let _websocketHeartbeatInterval: NodeJS.Timeout | null = null;\n let _lobbyHeartbeatInterval: NodeJS.Timeout | null = null;\n\n const DEFAULT_WEBSOCKET_HEARTBEAT_INTERVAL = 10000;\n const DEFAULT_LOBBY_HEARTBEAT_INTERVAL = 30000;\n const DEFAULT_COUNTDOWN_SECONDS_TO_START = 5;\n\n // Save if we are on dev environment so we don't need to use the runtimeGame every time.\n let isUsingGDevelopDevelopmentEnvironment = false;\n\n export let playerNumber: number | null = null;\n\n gdjs.registerRuntimeScenePreEventsCallback(\n (runtimeScene: gdjs.RuntimeScene) => {\n isUsingGDevelopDevelopmentEnvironment = runtimeScene\n .getGame()\n .isUsingGDevelopDevelopmentEnvironment();\n\n if (disableMultiplayerForTesting) return;\n\n gdjs.multiplayerMessageManager.handleHeartbeatsToSend();\n gdjs.multiplayerMessageManager.handleJustDisconnectedPeers(\n runtimeScene\n );\n\n gdjs.multiplayerMessageManager.handleChangeInstanceOwnerMessagesReceived(\n runtimeScene\n );\n gdjs.multiplayerMessageManager.handleUpdateInstanceMessagesReceived(\n runtimeScene\n );\n gdjs.multiplayerMessageManager.handleCustomMessagesReceived();\n gdjs.multiplayerMessageManager.handleAcknowledgeMessagesReceived();\n gdjs.multiplayerMessageManager.resendClearOrCancelAcknowledgedMessages(\n runtimeScene\n );\n gdjs.multiplayerMessageManager.handleChangeVariableOwnerMessagesReceived(\n runtimeScene\n );\n // In case we're joining an existing lobby, it's possible we haven't\n // fully caught up with the game state yet, especially if a scene is loading.\n // We look at them every frame, from the moment the lobby has started,\n // to ensure we don't miss any.\n if (_isLobbyGameRunning) {\n gdjs.multiplayerMessageManager.handleSavedUpdateMessages(\n runtimeScene\n );\n }\n gdjs.multiplayerMessageManager.handleUpdateGameMessagesReceived(\n runtimeScene\n );\n gdjs.multiplayerMessageManager.handleUpdateSceneMessagesReceived(\n runtimeScene\n );\n }\n );\n\n gdjs.registerRuntimeScenePostEventsCallback(\n (runtimeScene: gdjs.RuntimeScene) => {\n if (disableMultiplayerForTesting) return;\n\n // Handle joining and leaving players to show notifications accordingly.\n handleLeavingPlayer(runtimeScene);\n handleJoiningPlayer(runtimeScene);\n\n // Then look at the heartbeats received to know if a new player has joined/left.\n gdjs.multiplayerMessageManager.handleHeartbeatsReceived();\n\n gdjs.multiplayerMessageManager.handleDestroyInstanceMessagesReceived(\n runtimeScene\n );\n gdjs.multiplayerVariablesManager.handleChangeVariableOwnerMessagesToSend();\n gdjs.multiplayerMessageManager.handleUpdateGameMessagesToSend(\n runtimeScene\n );\n gdjs.multiplayerMessageManager.handleUpdateSceneMessagesToSend(\n runtimeScene\n );\n }\n );\n\n // Ensure that the condition \"game just started\" (or ended) is valid only for one frame.\n gdjs.registerRuntimeScenePostEventsCallback(() => {\n if (disableMultiplayerForTesting) return;\n\n _hasLobbyGameJustStarted = false;\n _hasLobbyGameJustEnded = false;\n });\n\n const getLobbiesWindowUrl = ({\n runtimeGame,\n gameId,\n }: {\n runtimeGame: gdjs.RuntimeGame;\n gameId: string;\n }) => {\n // Uncomment to test the case of a failing loading:\n // return 'https://gd.games.wronglink';\n\n const baseUrl = 'https://gd.games';\n // Uncomment to test locally:\n // const baseUrl = 'http://localhost:4000';\n\n const url = new URL(\n `${baseUrl}/games/${gameId}/lobbies${_lobbyId ? `/${_lobbyId}` : ''}`\n );\n url.searchParams.set(\n 'gameVersion',\n runtimeGame.getGameData().properties.version\n );\n if (runtimeGame.getAdditionalOptions().nativeMobileApp) {\n url.searchParams.set('nativeMobileApp', 'true');\n }\n url.searchParams.set(\n 'isPreview',\n runtimeGame.isPreview() ? 'true' : 'false'\n );\n if (isUsingGDevelopDevelopmentEnvironment) {\n url.searchParams.set('dev', 'true');\n }\n if (_connectionId) {\n url.searchParams.set('connectionId', _connectionId);\n }\n if (playerNumber) {\n url.searchParams.set('positionInLobby', playerNumber.toString());\n }\n const playerId = gdjs.playerAuthentication.getUserId();\n if (playerId) {\n url.searchParams.set('playerId', playerId);\n }\n const playerToken = gdjs.playerAuthentication.getUserToken();\n if (playerToken) {\n url.searchParams.set('playerToken', playerToken);\n }\n // Increment this value when a new feature is introduced so we can\n // adapt the interface of the lobbies.\n url.searchParams.set('multiplayerVersion', '2');\n\n return url.toString();\n };\n\n /**\n * Returns true if the game has just started,\n * useful to switch to the game scene.\n */\n export const hasLobbyGameJustStarted = () => _hasLobbyGameJustStarted;\n\n export const isLobbyGameRunning = () => _isLobbyGameRunning;\n\n export const isReadyToSendOrReceiveGameUpdateMessages = () =>\n _isReadyToSendOrReceiveGameUpdateMessages;\n\n /**\n * Returns true if the game has just ended,\n * useful to switch back to to the main menu.\n */\n export const hasLobbyGameJustEnded = () => _hasLobbyGameJustEnded;\n\n /**\n * Returns the number of players in the lobby.\n */\n export const getPlayersInLobbyCount = () => {\n // Whether the lobby game has started or not, the number of players in the lobby\n // is the number of connected players.\n return gdjs.multiplayerMessageManager.getNumberOfConnectedPlayers();\n };\n\n /**\n * Returns true if the player at this position is connected to the lobby.\n */\n export const isPlayerConnected = (playerNumber: number) => {\n return gdjs.multiplayerMessageManager.isPlayerConnected(playerNumber);\n };\n\n /**\n * Returns the position of the current player in the lobby.\n * Return 0 if the player is not in the lobby.\n * Returns 1, 2, 3, ... if the player is in the lobby.\n */\n export const getCurrentPlayerNumber = () => {\n return playerNumber || 0;\n };\n\n /**\n * Returns true if the player is the host in the lobby. Here, player 1.\n */\n export const isPlayerHost = () => {\n return playerNumber === 1;\n };\n\n /**\n * Returns the player username at the given number in the lobby.\n * The number is shifted by one, so that the first player has number 1.\n */\n export const getPlayerUsername = (playerNumber: number) => {\n return gdjs.multiplayerMessageManager.getPlayerUsername(playerNumber);\n };\n\n /**\n * Returns the player username of the current player in the lobby.\n */\n export const getCurrentPlayerUsername = () => {\n const currentPlayerNumber = getCurrentPlayerNumber();\n return getPlayerUsername(currentPlayerNumber);\n };\n\n const handleLeavingPlayer = (runtimeScene: gdjs.RuntimeScene) => {\n const lastestPlayerWhoJustLeft = gdjs.multiplayerMessageManager.getLatestPlayerWhoJustLeft();\n if (lastestPlayerWhoJustLeft) {\n const playerUsername = getPlayerUsername(lastestPlayerWhoJustLeft);\n gdjs.multiplayerComponents.displayPlayerLeftNotification(\n runtimeScene,\n playerUsername\n );\n // We remove the players who just left 1 by 1, so that they can be treated in different frames.\n // This is especially important if the expression to know the latest player who just left is used,\n // to avoid missing a player leaving.\n gdjs.multiplayerMessageManager.removePlayerWhoJustLeft();\n\n // When a player leaves, we send a heartbeat to the backend so that they're aware of the players in the lobby.\n // Do not await as we want don't want to block the execution of the of the rest of the logic.\n sendHeartbeatToBackend();\n }\n };\n\n const handleJoiningPlayer = (runtimeScene: gdjs.RuntimeScene) => {\n const lastestPlayerWhoJustJoined = gdjs.multiplayerMessageManager.getLatestPlayerWhoJustJoined();\n if (lastestPlayerWhoJustJoined) {\n const playerUsername = getPlayerUsername(lastestPlayerWhoJustJoined);\n gdjs.multiplayerComponents.displayPlayerJoinedNotification(\n runtimeScene,\n playerUsername\n );\n }\n // We remove the players who just joined 1 by 1, so that they can be treated in different frames.\n // This is especially important if the expression to know the latest player who just joined is used,\n // to avoid missing a player joining.\n gdjs.multiplayerMessageManager.removePlayerWhoJustJoined();\n };\n\n /**\n * Returns true if the game is registered, false otherwise.\n * Useful to display a message to the user to register the game before logging in.\n */\n const checkIfGameIsRegistered = (\n runtimeGame: gdjs.RuntimeGame,\n gameId: string,\n tries: number = 0\n ): Promise<boolean> => {\n const rootApi = isUsingGDevelopDevelopmentEnvironment\n ? 'https://api-dev.gdevelop.io'\n : 'https://api.gdevelop.io';\n const url = `${rootApi}/game/public-game/${gameId}`;\n return fetch(url, { method: 'HEAD' }).then(\n (response) => {\n if (response.status !== 200) {\n logger.warn(\n `Error while fetching the game: ${response.status} ${response.statusText}`\n );\n\n // If the response is not 404, it may be a timeout, so retry a few times.\n if (response.status === 404 || tries > 2) {\n return false;\n }\n\n return checkIfGameIsRegistered(runtimeGame, gameId, tries + 1);\n }\n return true;\n },\n (err) => {\n logger.error('Error while fetching game:', err);\n return false;\n }\n );\n };\n\n const handleJoinLobbyEvent = function (\n runtimeScene: gdjs.RuntimeScene,\n lobbyId: string\n ) {\n if (_connectionId) {\n logger.info('Already connected to a lobby.');\n return;\n }\n\n if (_websocket) {\n logger.warn('Already connected to a lobby. Closing the previous one.');\n _websocket.close();\n _connectionId = null;\n playerNumber = null;\n _lobbyId = null;\n _websocket = null;\n }\n\n const gameId = gdjs.projectData.properties.projectUuid;\n const playerId = gdjs.playerAuthentication.getUserId();\n const playerToken = gdjs.playerAuthentication.getUserToken();\n if (!gameId) {\n logger.error('Cannot open lobbies if the project has no ID.');\n return;\n }\n if (!playerId || !playerToken) {\n logger.warn('Cannot open lobbies if the player is not connected.');\n return;\n }\n const wsPlayApi = isUsingGDevelopDevelopmentEnvironment\n ? 'wss://api-ws-dev.gdevelop.io/play'\n : 'wss://api-ws.gdevelop.io/play';\n\n const wsUrl = new URL(wsPlayApi);\n wsUrl.searchParams.set('gameId', gameId);\n wsUrl.searchParams.set('lobbyId', lobbyId);\n wsUrl.searchParams.set('playerId', playerId);\n wsUrl.searchParams.set('connectionType', 'lobby');\n wsUrl.searchParams.set('playerGameToken', playerToken);\n _websocket = new WebSocket(wsUrl.toString());\n _websocket.onopen = () => {\n logger.info('Connected to the lobby.');\n // Register a heartbeat to keep the connection alive.\n _websocketHeartbeatInterval = setInterval(() => {\n if (_websocket) {\n _websocket.send(\n JSON.stringify({\n action: 'heartbeat',\n connectionType: 'lobby',\n })\n );\n }\n }, DEFAULT_WEBSOCKET_HEARTBEAT_INTERVAL);\n\n // When socket is open, ask for the connectionId and send more session info, so that we can inform the lobbies window.\n if (_websocket) {\n _websocket.send(JSON.stringify({ action: 'getConnectionId' }));\n const platformInfo = runtimeScene.getGame().getPlatformInfo();\n _websocket.send(\n JSON.stringify({\n action: 'sessionInformation',\n connectionType: 'lobby',\n isCordova: platformInfo.isCordova,\n devicePlatform: platformInfo.devicePlatform,\n navigatorPlatform: platformInfo.navigatorPlatform,\n hasTouch: platformInfo.hasTouch,\n supportedCompressionMethods:\n platformInfo.supportedCompressionMethods,\n })\n );\n }\n };\n _websocket.onmessage = (event) => {\n if (event.data) {\n const messageContent = JSON.parse(event.data);\n switch (messageContent.type) {\n case 'connectionId': {\n const messageData = messageContent.data;\n const connectionId = messageData.connectionId;\n const positionInLobby = messageData.positionInLobby;\n const validIceServers = messageData.validIceServers || [];\n const brokerServerConfig = messageData.brokerServerConfig;\n\n if (!connectionId || !positionInLobby) {\n logger.error('No connectionId or position received');\n gdjs.multiplayerComponents.displayErrorNotification(\n runtimeScene\n );\n // Close the websocket as something wrong happened.\n if (_websocket) _websocket.close();\n return;\n }\n\n handleConnectionIdReceived({\n runtimeScene,\n connectionId,\n positionInLobby,\n lobbyId,\n playerId,\n playerToken,\n validIceServers,\n brokerServerConfig,\n });\n break;\n }\n case 'lobbyUpdated': {\n const messageData = messageContent.data;\n const positionInLobby = messageData.positionInLobby;\n handleLobbyUpdatedEvent({\n runtimeScene,\n positionInLobby,\n });\n break;\n }\n case 'gameCountdownStarted': {\n const messageData = messageContent.data;\n const compressionMethod = messageData.compressionMethod || 'none';\n const secondsToStart =\n messageData.secondsToStart ||\n DEFAULT_COUNTDOWN_SECONDS_TO_START;\n handleGameCountdownStartedEvent({\n runtimeScene,\n compressionMethod,\n secondsToStart,\n });\n break;\n }\n case 'gameStarted': {\n const messageData = messageContent.data;\n const heartbeatInterval =\n messageData.heartbeatInterval ||\n DEFAULT_LOBBY_HEARTBEAT_INTERVAL;\n\n handleGameStartedEvent({ runtimeScene, heartbeatInterval });\n break;\n }\n case 'peerId': {\n const messageData = messageContent.data;\n if (!messageData) {\n logger.error('No message received');\n return;\n }\n const peerId = messageData.peerId;\n const compressionMethod = messageData.compressionMethod;\n if (!peerId || !compressionMethod) {\n logger.error('Malformed message received');\n return;\n }\n\n handlePeerIdEvent({ peerId, compressionMethod });\n break;\n }\n }\n }\n };\n _websocket.onclose = () => {\n logger.info(\n 'Disconnected from the lobby. Either manually or game started.'\n );\n\n _connectionId = null;\n _websocket = null;\n if (_websocketHeartbeatInterval) {\n clearInterval(_websocketHeartbeatInterval);\n }\n\n // If the game is running, then all good.\n // Otherwise, the player left the lobby.\n if (_isLobbyGameRunning) {\n return;\n }\n\n const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(\n runtimeScene\n );\n\n if (!lobbiesIframe || !lobbiesIframe.contentWindow) {\n return;\n }\n\n // Tell the Lobbies iframe that the lobby has been left.\n lobbiesIframe.contentWindow.postMessage(\n {\n id: 'lobbyLeft',\n },\n '*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.\n );\n };\n };\n\n const handleConnectionIdReceived = function ({\n runtimeScene,\n connectionId,\n positionInLobby,\n lobbyId,\n playerId,\n playerToken,\n validIceServers,\n brokerServerConfig,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n connectionId: string;\n positionInLobby: number;\n lobbyId: string;\n playerId: string;\n playerToken: string;\n validIceServers: {\n urls: string;\n username?: string;\n credential?: string;\n }[];\n brokerServerConfig?: {\n hostname: string;\n port: number;\n path: string;\n key: string;\n secure: boolean;\n };\n }) {\n // When the connectionId is received, initialise PeerJS so players can connect to each others afterwards.\n if (validIceServers.length) {\n for (const server of validIceServers) {\n gdjs.multiplayerPeerJsHelper.useCustomICECandidate(\n server.urls,\n server.username,\n server.credential\n );\n }\n }\n if (brokerServerConfig) {\n gdjs.multiplayerPeerJsHelper.useCustomBrokerServer(\n brokerServerConfig.hostname,\n brokerServerConfig.port,\n brokerServerConfig.path,\n brokerServerConfig.key,\n brokerServerConfig.secure\n );\n } else {\n gdjs.multiplayerPeerJsHelper.useDefaultBrokerServer();\n }\n\n _connectionId = connectionId;\n playerNumber = positionInLobby;\n // We save the lobbyId here as this is the moment when the player is really connected to the lobby.\n _lobbyId = lobbyId;\n\n // Then we inform the lobbies window that the player has joined.\n const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(\n runtimeScene\n );\n\n if (!lobbiesIframe || !lobbiesIframe.contentWindow) {\n logger.error(\n 'The lobbies iframe is not opened, cannot send the join message.'\n );\n return;\n }\n\n lobbiesIframe.contentWindow.postMessage(\n {\n id: 'lobbyJoined',\n lobbyId,\n playerId,\n playerToken,\n connectionId: _connectionId,\n positionInLobby,\n },\n // Specify the origin to avoid leaking the playerToken.\n // Replace with '*' to test locally.\n 'https://gd.games'\n // '*'\n );\n };\n\n const handleLeaveLobbyEvent = function () {\n if (_websocket) {\n _websocket.close();\n }\n _connectionId = null;\n playerNumber = null;\n _lobbyId = null;\n _websocket = null;\n };\n\n const handleLobbyUpdatedEvent = function ({\n runtimeScene,\n positionInLobby,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n positionInLobby: number;\n }) {\n // This is mainly useful when joining a lobby, or when the lobby is updated before the game starts.\n // The position in lobby should never change after the game has started (the WS is closed anyway).\n playerNumber = positionInLobby;\n\n // If the player is in the lobby, tell the lobbies window that the lobby has been updated,\n // as well as the player position.\n const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(\n runtimeScene\n );\n\n if (!lobbiesIframe || !lobbiesIframe.contentWindow) {\n return;\n }\n\n lobbiesIframe.contentWindow.postMessage(\n {\n id: 'lobbyUpdated',\n positionInLobby,\n },\n '*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.\n );\n };\n\n const handleGameCountdownStartedEvent = function ({\n runtimeScene,\n compressionMethod,\n secondsToStart,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n compressionMethod: gdjs.multiplayerPeerJsHelper.CompressionMethod;\n secondsToStart: number;\n }) {\n gdjs.multiplayerPeerJsHelper.setCompressionMethod(compressionMethod);\n\n // When the countdown starts, if we are player number 1, then send the peerId to others so they can connect via P2P.\n if (getCurrentPlayerNumber() === 1) {\n sendPeerId();\n }\n\n // Just pass along the message to the iframe so that it can display the countdown.\n const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(\n runtimeScene\n );\n\n if (!lobbiesIframe || !lobbiesIframe.contentWindow) {\n logger.info('The lobbies iframe is not opened, not sending message.');\n return;\n }\n\n lobbiesIframe.contentWindow.postMessage(\n {\n id: 'gameCountdownStarted',\n secondsToStart,\n },\n '*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.\n );\n\n // Prevent the player from leaving the lobby while the game is starting.\n gdjs.multiplayerComponents.hideLobbiesCloseButtonTemporarily(\n runtimeScene\n );\n };\n\n const sendHeartbeatToBackend = async function () {\n const gameId = gdjs.projectData.properties.projectUuid;\n const playerId = gdjs.playerAuthentication.getUserId();\n const playerToken = gdjs.playerAuthentication.getUserToken();\n\n if (!gameId || !playerId || !playerToken || !_lobbyId) {\n logger.error(\n 'Cannot keep the lobby playing without the game ID or player ID.'\n );\n return;\n }\n\n const rootApi = isUsingGDevelopDevelopmentEnvironment\n ? 'https://api-dev.gdevelop.io'\n : 'https://api.gdevelop.io';\n const headers = {\n 'Content-Type': 'application/json',\n };\n let heartbeatUrl = `${rootApi}/play/game/${gameId}/public-lobby/${_lobbyId}/action/heartbeat`;\n headers['Authorization'] = `player-game-token ${playerToken}`;\n heartbeatUrl += `?playerId=${playerId}`;\n const players = gdjs.multiplayerMessageManager.getConnectedPlayers();\n try {\n await fetch(heartbeatUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n players,\n }),\n });\n } catch (error) {\n logger.error('Error while sending heartbeat, retrying:', error);\n try {\n await fetch(heartbeatUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n players,\n }),\n });\n } catch (error) {\n logger.error(\n 'Error while sending heartbeat a second time. Giving up:',\n error\n );\n }\n }\n };\n\n /**\n * When the game receives the information that the game has started, close the\n * lobbies window, focus on the game, and set the flag to true.\n */\n const handleGameStartedEvent = function ({\n runtimeScene,\n heartbeatInterval,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n heartbeatInterval: number;\n }) {\n // It is possible the connection to other players didn't work.\n // If that's the case, show an error message and leave the lobby.\n // If we are the host, still start the game, as this allows a player to test the game alone.\n const allConnectedPeers = gdjs.multiplayerPeerJsHelper.getAllPeers();\n if (!isPlayerHost() && allConnectedPeers.length === 0) {\n gdjs.multiplayerComponents.displayConnectionErrorNotification(\n runtimeScene\n );\n // Do as if the player left the lobby.\n handleLeaveLobbyEvent();\n removeLobbiesContainer(runtimeScene);\n focusOnGame(runtimeScene);\n return;\n }\n\n // If we are the host, start pinging the backend to let it know the lobby is running.\n if (isPlayerHost()) {\n _lobbyHeartbeatInterval = setInterval(async () => {\n await sendHeartbeatToBackend();\n }, heartbeatInterval);\n }\n\n // If we are connected to players, then the game can start.\n logger.info('Lobby game has started.');\n // In case we're joining an existing lobby, read the saved messages to catch-up with the game state.\n gdjs.multiplayerMessageManager.handleSavedUpdateMessages(runtimeScene);\n _isReadyToSendOrReceiveGameUpdateMessages = true;\n _hasLobbyGameJustStarted = true;\n _isLobbyGameRunning = true;\n removeLobbiesContainer(runtimeScene);\n // Close the websocket, as we don't need it anymore.\n if (_websocket) {\n _websocket.close();\n }\n focusOnGame(runtimeScene);\n };\n\n /**\n * When the game receives the information that the game has ended, set the flag to true,\n * so that the game can switch back to the main menu for instance.\n */\n export const handleLobbyGameEnded = function () {\n logger.info('Lobby game has ended.');\n _hasLobbyGameJustEnded = true;\n _isLobbyGameRunning = false;\n _lobbyId = null;\n playerNumber = null;\n _isReadyToSendOrReceiveGameUpdateMessages = false;\n if (_lobbyHeartbeatInterval) {\n clearInterval(_lobbyHeartbeatInterval);\n }\n\n // Disconnect from any P2P connections.\n gdjs.multiplayerPeerJsHelper.disconnectFromAllPeers();\n\n // Clear the expected acknowledgments, as the game is ending.\n gdjs.multiplayerMessageManager.clearAllMessagesTempData();\n };\n\n /**\n * When the game receives the information of the peerId, then\n * the player can connect to the peer.\n */\n const handlePeerIdEvent = function ({\n peerId,\n compressionMethod,\n }: {\n peerId: string;\n compressionMethod: gdjs.multiplayerPeerJsHelper.CompressionMethod;\n }) {\n // When a peerId is received, trigger a P2P connection with the peer, just after setting the compression method.\n gdjs.multiplayerPeerJsHelper.setCompressionMethod(compressionMethod);\n const currentPeerId = gdjs.multiplayerPeerJsHelper.getCurrentId();\n if (!currentPeerId) {\n logger.error(\n 'No peerId found, the player does not seem connected to the broker server.'\n );\n return;\n }\n\n if (currentPeerId === peerId) {\n logger.info('Received our own peerId, ignoring.');\n return;\n }\n\n gdjs.multiplayerPeerJsHelper.connect(peerId);\n };\n\n /**\n * When the game receives a start countdown message from the lobby, just send it to all\n * players in the lobby via the websocket.\n * It will then trigger an event from the websocket to all players in the lobby.\n */\n const handleStartGameCountdownMessage = function () {\n if (!_websocket) {\n logger.error(\n 'No connection to send the start countdown message. Are you connected to a lobby?'\n );\n return;\n }\n\n _websocket.send(\n JSON.stringify({\n action: 'startGameCountdown',\n connectionType: 'lobby',\n })\n );\n };\n\n /**\n * When the game receives a start game message from the lobby, just send it to all\n * players in the lobby via the websocket.\n * It will then trigger an event from the websocket to all players in the lobby.\n */\n const handleStartGameMessage = function () {\n if (!_websocket) {\n logger.error(\n 'No connection to send the start countdown message. Are you connected to a lobby?'\n );\n return;\n }\n\n _websocket.send(\n JSON.stringify({\n action: 'startGame',\n connectionType: 'lobby',\n })\n );\n\n // As the host, start sending messages to the players.\n _isReadyToSendOrReceiveGameUpdateMessages = true;\n };\n\n /**\n * When the game receives a join game message from the lobby, send it via the WS\n * waiting for a peerId to be received and that the connection happens automatically.\n */\n const handleJoinGameMessage = function () {\n if (!_websocket) {\n logger.error(\n 'No connection to send the start countdown message. Are you connected to a lobby?'\n );\n return;\n }\n\n _websocket.send(\n JSON.stringify({\n action: 'joinGame',\n connectionType: 'lobby',\n })\n );\n };\n\n /**\n * When the first heartbeat is received, we consider the connection to the host as working,\n * we inform the backend services that the connection is ready, so it can start the game when\n * everyone is ready.\n */\n export const markConnectionAsConnected = function () {\n if (!_websocket) {\n return;\n }\n\n _websocket.send(\n JSON.stringify({\n action: 'updateConnection',\n connectionType: 'lobby',\n status: 'connected',\n })\n );\n };\n\n /**\n * Action to end the lobby game.\n * This will update the lobby status and inform everyone in the lobby that the game has ended.\n */\n export const endLobbyGame = async function () {\n if (!isLobbyGameRunning()) {\n return;\n }\n\n if (!isPlayerHost()) {\n logger.error('Only the host can end the game.');\n return;\n }\n\n // Consider the game is ended, so that we don't listen to other players disconnecting.\n _isLobbyGameRunning = false;\n\n logger.info('Ending the lobby game.');\n\n // Inform the players that the game has ended.\n gdjs.multiplayerMessageManager.sendEndGameMessage();\n\n // Also call backend to end the game.\n const gameId = gdjs.projectData.properties.projectUuid;\n const playerId = gdjs.playerAuthentication.getUserId();\n const playerToken = gdjs.playerAuthentication.getUserToken();\n\n if (!gameId || !playerId || !playerToken || !_lobbyId) {\n logger.error('Cannot end the lobby without the game ID or player ID.');\n return;\n }\n\n const rootApi = isUsingGDevelopDevelopmentEnvironment\n ? 'https://api-dev.gdevelop.io'\n : 'https://api.gdevelop.io';\n const headers = {\n 'Content-Type': 'application/json',\n };\n let endGameUrl = `${rootApi}/play/game/${gameId}/public-lobby/${_lobbyId}/action/end`;\n headers['Authorization'] = `player-game-token ${playerToken}`;\n endGameUrl += `?playerId=${playerId}`;\n try {\n await fetch(endGameUrl, {\n method: 'POST',\n headers,\n body: JSON.stringify({\n gameId,\n lobbyId: _lobbyId,\n }),\n });\n } catch (error) {\n logger.error('Error while ending the game:', error);\n }\n\n // Do as if everyone left the lobby.\n handleLobbyGameEnded();\n };\n\n /**\n * Helper to send the ID from PeerJS to the lobby players.\n */\n const sendPeerId = function () {\n if (!_websocket) {\n logger.error(\n 'No connection to send the message. Are you connected to a lobby?'\n );\n return;\n }\n\n const peerId = gdjs.multiplayerPeerJsHelper.getCurrentId();\n if (!peerId) {\n logger.error(\n \"No peerId found, the player doesn't seem connected to the broker server.\"\n );\n return;\n }\n\n _websocket.send(\n JSON.stringify({\n action: 'sendPeerId',\n connectionType: 'lobby',\n peerId,\n })\n );\n };\n\n /**\n * Reads the event sent by the lobbies window and\n * react accordingly.\n */\n const receiveLobbiesMessage = function (\n runtimeScene: gdjs.RuntimeScene,\n event: MessageEvent,\n { checkOrigin }: { checkOrigin: boolean }\n ) {\n const allowedOrigins = ['https://gd.games', 'http://localhost:4000'];\n\n // Check origin of message.\n if (checkOrigin && !allowedOrigins.includes(event.origin)) {\n // Wrong origin. Return silently.\n return;\n }\n // Check that message is not malformed.\n if (!event.data.id) {\n throw new Error('Malformed message');\n }\n\n // Handle message.\n switch (event.data.id) {\n case 'lobbiesListenerReady': {\n sendSessionInformation(runtimeScene);\n break;\n }\n case 'joinLobby': {\n if (!event.data.lobbyId) {\n throw new Error('Malformed message.');\n }\n\n handleJoinLobbyEvent(runtimeScene, event.data.lobbyId);\n break;\n }\n case 'startGameCountdown': {\n handleStartGameCountdownMessage();\n break;\n }\n case 'startGame': {\n handleStartGameMessage();\n break;\n }\n case 'leaveLobby': {\n handleLeaveLobbyEvent();\n break;\n }\n case 'joinGame': {\n handleJoinGameMessage();\n break;\n }\n }\n };\n\n /**\n * Handle any error that can occur as part of displaying the lobbies.\n */\n const handleLobbiesError = function (\n runtimeScene: gdjs.RuntimeScene,\n message: string\n ) {\n logger.error(message);\n removeLobbiesContainer(runtimeScene);\n focusOnGame(runtimeScene);\n };\n\n const sendSessionInformation = (runtimeScene: gdjs.RuntimeScene) => {\n const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(\n runtimeScene\n );\n if (!lobbiesIframe || !lobbiesIframe.contentWindow) {\n // Cannot send the message if the iframe is not opened.\n return;\n }\n\n const platformInfo = runtimeScene.getGame().getPlatformInfo();\n\n lobbiesIframe.contentWindow.postMessage(\n {\n id: 'sessionInformation',\n isCordova: platformInfo.isCordova,\n devicePlatform: platformInfo.devicePlatform,\n navigatorPlatform: platformInfo.navigatorPlatform,\n hasTouch: platformInfo.hasTouch,\n },\n '*'\n );\n };\n\n /**\n * Helper to handle lobbies iframe.\n * We open an iframe, and listen to messages posted back to the game window.\n */\n const openLobbiesIframe = (\n runtimeScene: gdjs.RuntimeScene,\n gameId: string\n ) => {\n const targetUrl = getLobbiesWindowUrl({\n runtimeGame: runtimeScene.getGame(),\n gameId,\n });\n\n // Listen to messages posted by the lobbies window, so that we can\n // know when they join or leave a lobby.\n _lobbiesMessageCallback = (event: MessageEvent) => {\n receiveLobbiesMessage(runtimeScene, event, {\n checkOrigin: true,\n });\n };\n window.addEventListener('message', _lobbiesMessageCallback, true);\n\n gdjs.multiplayerComponents.displayIframeInsideLobbiesContainer(\n runtimeScene,\n targetUrl\n );\n };\n\n /**\n * Action to display the lobbies window to the user.\n */\n export const openLobbiesWindow = async (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (\n isLobbiesWindowOpen(runtimeScene) ||\n gdjs.playerAuthentication.isAuthenticationWindowOpen()\n ) {\n return;\n }\n\n const _gameId = gdjs.projectData.properties.projectUuid;\n if (!_gameId) {\n handleLobbiesError(\n runtimeScene,\n 'The game ID is missing, the lobbies window cannot be opened.'\n );\n return;\n }\n\n if (_isCheckingIfGameIsRegistered || _isWaitingForLogin) {\n // The action is called multiple times, let's prevent that.\n return;\n }\n\n // Create the lobbies container for the player to wait.\n const domElementContainer = runtimeScene\n .getGame()\n .getRenderer()\n .getDomElementContainer();\n if (!domElementContainer) {\n handleLobbiesError(\n runtimeScene,\n \"The div element covering the game couldn't be found, the lobbies window cannot be displayed.\"\n );\n return;\n }\n\n const onLobbiesContainerDismissed = () => {\n removeLobbiesContainer(runtimeScene);\n };\n\n const playerId = gdjs.playerAuthentication.getUserId();\n const playerToken = gdjs.playerAuthentication.getUserToken();\n if (!playerId || !playerToken) {\n _isWaitingForLogin = true;\n const {\n status,\n } = await gdjs.playerAuthentication.openAuthenticationWindow(\n runtimeScene\n ).promise;\n _isWaitingForLogin = false;\n\n if (status === 'logged') {\n openLobbiesWindow(runtimeScene);\n }\n\n return;\n }\n\n gdjs.multiplayerComponents.displayLobbies(\n runtimeScene,\n onLobbiesContainerDismissed\n );\n\n // If the game is registered, open the lobbies window.\n // Otherwise, open the window indicating that the game is not registered.\n if (_isGameRegistered === null) {\n _isCheckingIfGameIsRegistered = true;\n try {\n const isGameRegistered = await checkIfGameIsRegistered(\n runtimeScene.getGame(),\n _gameId\n );\n _isGameRegistered = isGameRegistered;\n } catch (error) {\n _isGameRegistered = false;\n logger.error(\n 'Error while checking if the game is registered:',\n error\n );\n handleLobbiesError(\n runtimeScene,\n 'Error while checking if the game is registered.'\n );\n return;\n } finally {\n _isCheckingIfGameIsRegistered = false;\n }\n }\n const electron = runtimeScene.getGame().getRenderer().getElectron();\n const wikiOpenAction = electron\n ? () =>\n electron.shell.openExternal(\n 'https://wiki.gdevelop.io/gdevelop5/publishing/web'\n )\n : () =>\n window.open(\n 'https://wiki.gdevelop.io/gdevelop5/publishing/web',\n '_blank'\n );\n\n gdjs.multiplayerComponents.addTextsToLoadingContainer(\n runtimeScene,\n _isGameRegistered,\n wikiOpenAction\n );\n\n if (_isGameRegistered) {\n openLobbiesIframe(runtimeScene, _gameId);\n }\n };\n\n /**\n * Condition to check if the window is open, so that the game can be paused in the background.\n */\n export const isLobbiesWindowOpen = function (\n runtimeScene: gdjs.RuntimeScene\n ): boolean {\n const lobbiesRootContainer = gdjs.multiplayerComponents.getLobbiesRootContainer(\n runtimeScene\n );\n return !!lobbiesRootContainer;\n };\n\n export const showLobbiesCloseButton = function (\n runtimeScene: gdjs.RuntimeScene,\n visible: boolean\n ) {\n gdjs.multiplayerComponents.changeLobbiesWindowCloseActionVisibility(\n runtimeScene,\n visible\n );\n };\n\n /**\n * Remove the container displaying the lobbies window and the callback.\n */\n export const removeLobbiesContainer = function (\n runtimeScene: gdjs.RuntimeScene\n ) {\n removeLobbiesCallbacks();\n gdjs.multiplayerComponents.removeLobbiesContainer(runtimeScene);\n };\n\n /*\n * Remove the lobbies callbacks.\n */\n const removeLobbiesCallbacks = function () {\n // Remove the lobbies callbacks.\n if (_lobbiesMessageCallback) {\n window.removeEventListener('message', _lobbiesMessageCallback, true);\n _lobbiesMessageCallback = null;\n }\n };\n\n /**\n * Focus on game canvas to allow user to interact with it.\n */\n const focusOnGame = function (runtimeScene: gdjs.RuntimeScene) {\n const gameCanvas = runtimeScene.getGame().getRenderer().getCanvas();\n if (gameCanvas) gameCanvas.focus();\n };\n\n /**\n * Action to allow the player to leave the lobby in-game.\n */\n export const leaveGameLobby = async () => {\n // Handle the case where the game has not started yet, so the player is in the lobby.\n handleLeaveLobbyEvent();\n // Handle the case where the game has started, so the player is in the game and connected to other players.\n handleLobbyGameEnded();\n };\n }\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
4
|
+
"sourcesContent": ["namespace gdjs {\n const logger = new gdjs.Logger('Multiplayer');\n\n type LobbyChangeHostRequest = {\n lobbyId: string;\n gameId: string;\n peerId: string;\n playerId: string;\n ping: number;\n createdAt: number;\n ttl: number;\n newLobbyId?: string;\n newHostPeerId?: string;\n newPlayers?: {\n playerNumber: number;\n playerId: string;\n }[];\n };\n\n const getTimeNow =\n window.performance && typeof window.performance.now === 'function'\n ? window.performance.now.bind(window.performance)\n : Date.now;\n\n const fetchAsPlayer = async ({\n relativeUrl,\n method,\n body,\n dev,\n }: {\n relativeUrl: string;\n method: 'GET' | 'POST';\n body?: string;\n dev: boolean;\n }) => {\n const playerId = gdjs.playerAuthentication.getUserId();\n const playerToken = gdjs.playerAuthentication.getUserToken();\n if (!playerId || !playerToken) {\n logger.warn('Cannot fetch as a player if the player is not connected.');\n throw new Error(\n 'Cannot fetch as a player if the player is not connected.'\n );\n }\n\n const rootApi = dev\n ? 'https://api-dev.gdevelop.io'\n : 'https://api.gdevelop.io';\n const url = new URL(`${rootApi}${relativeUrl}`);\n url.searchParams.set('playerId', playerId);\n const formattedUrl = url.toString();\n\n const headers = {\n 'Content-Type': 'application/json',\n Authorization: `player-game-token ${playerToken}`,\n };\n const response = await fetch(formattedUrl, {\n method,\n headers,\n body,\n });\n if (!response.ok) {\n throw new Error(\n `Error while fetching as a player: ${response.status} ${response.statusText}`\n );\n }\n\n // Response can either be 'OK' or a JSON object. Get the content before trying to parse it.\n const responseText = await response.text();\n if (responseText === 'OK') {\n return;\n }\n\n try {\n return JSON.parse(responseText);\n } catch (error) {\n throw new Error(`Error while parsing the response: ${error}`);\n }\n };\n\n export namespace multiplayer {\n /** Set to true in testing to avoid relying on the multiplayer extension. */\n export let disableMultiplayerForTesting = false;\n\n export let _isReadyToSendOrReceiveGameUpdateMessages = false;\n\n let _isGameRegistered: boolean | null = null;\n let _isCheckingIfGameIsRegistered = false;\n let _isWaitingForLogin = false;\n\n let _hasLobbyGameJustStarted = false;\n export let _isLobbyGameRunning = false;\n let _hasLobbyGameJustEnded = false;\n let _lobbyId: string | null = null;\n let _connectionId: string | null = null;\n\n let _shouldEndLobbyWhenHostLeaves = false;\n let _lobbyChangeHostRequest: LobbyChangeHostRequest | null = null;\n let _lobbyChangeHostRequestInitiatedAt: number | null = null;\n let _isChangingHost = false;\n let _lobbyNewHostPickedAt: number | null = null;\n\n // Communication methods.\n let _lobbiesMessageCallback: ((event: MessageEvent) => void) | null = null;\n let _websocket: WebSocket | null = null;\n let _websocketHeartbeatIntervalFunction: NodeJS.Timeout | null = null;\n let _lobbyHeartbeatIntervalFunction: NodeJS.Timeout | null = null;\n\n const DEFAULT_WEBSOCKET_HEARTBEAT_INTERVAL = 10000;\n const DEFAULT_LOBBY_HEARTBEAT_INTERVAL = 30000;\n let currentLobbyHeartbeatInterval = DEFAULT_LOBBY_HEARTBEAT_INTERVAL;\n const DEFAULT_LOBBY_CHANGE_HOST_REQUEST_CHECK_INTERVAL = 1000;\n // 10 seconds to be safe, but the backend will answer in less.\n const DEFAULT_LOBBY_CHANGE_HOST_REQUEST_TIMEOUT = 10000;\n const DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_CHECK_INTERVAL = 1000;\n const DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_TIMEOUT = 10000;\n let _resumeTimeout: NodeJS.Timeout | null = null;\n const DEFAULT_LOBBY_EXPECTED_RESUME_TIMEOUT = 12000;\n\n export const DEFAULT_OBJECT_MAX_SYNC_RATE = 30;\n // The number of times per second an object should be synchronized if it keeps changing.\n export let _objectMaxSyncRate = DEFAULT_OBJECT_MAX_SYNC_RATE;\n\n // Save if we are on dev environment so we don't need to use the runtimeGame every time.\n let isUsingGDevelopDevelopmentEnvironment = false;\n\n export let playerNumber: number | null = null;\n export let hostPeerId: string | null = null;\n\n gdjs.registerRuntimeScenePreEventsCallback(\n (runtimeScene: gdjs.RuntimeScene) => {\n isUsingGDevelopDevelopmentEnvironment = runtimeScene\n .getGame()\n .isUsingGDevelopDevelopmentEnvironment();\n\n if (disableMultiplayerForTesting) return;\n\n gdjs.multiplayerMessageManager.handleHeartbeatsToSend();\n gdjs.multiplayerMessageManager.handleJustDisconnectedPeers(\n runtimeScene\n );\n\n gdjs.multiplayerMessageManager.handleChangeInstanceOwnerMessagesReceived(\n runtimeScene\n );\n gdjs.multiplayerMessageManager.handleUpdateInstanceMessagesReceived(\n runtimeScene\n );\n gdjs.multiplayerMessageManager.handleCustomMessagesReceived();\n gdjs.multiplayerMessageManager.handleAcknowledgeMessagesReceived();\n gdjs.multiplayerMessageManager.resendClearOrCancelAcknowledgedMessages(\n runtimeScene\n );\n gdjs.multiplayerMessageManager.handleChangeVariableOwnerMessagesReceived(\n runtimeScene\n );\n // In case we're joining an existing lobby, it's possible we haven't\n // fully caught up with the game state yet, especially if a scene is loading.\n // We look at them every frame, from the moment the lobby has started,\n // to ensure we don't miss any.\n if (_isLobbyGameRunning) {\n gdjs.multiplayerMessageManager.handleSavedUpdateMessages(\n runtimeScene\n );\n }\n gdjs.multiplayerMessageManager.handleUpdateGameMessagesReceived(\n runtimeScene\n );\n gdjs.multiplayerMessageManager.handleUpdateSceneMessagesReceived(\n runtimeScene\n );\n }\n );\n\n gdjs.registerRuntimeScenePostEventsCallback(\n (runtimeScene: gdjs.RuntimeScene) => {\n if (disableMultiplayerForTesting) return;\n\n // Handle joining and leaving players to show notifications accordingly.\n handleLeavingPlayer(runtimeScene);\n handleJoiningPlayer(runtimeScene);\n\n // Then look at the heartbeats received to know if a new player has joined/left.\n gdjs.multiplayerMessageManager.handleHeartbeatsReceived();\n\n gdjs.multiplayerMessageManager.handleEndGameMessagesReceived();\n gdjs.multiplayerMessageManager.handleResumeGameMessagesReceived(\n runtimeScene\n );\n\n gdjs.multiplayerMessageManager.handleDestroyInstanceMessagesReceived(\n runtimeScene\n );\n gdjs.multiplayerVariablesManager.handleChangeVariableOwnerMessagesToSend();\n gdjs.multiplayerMessageManager.handleUpdateGameMessagesToSend(\n runtimeScene\n );\n gdjs.multiplayerMessageManager.handleUpdateSceneMessagesToSend(\n runtimeScene\n );\n }\n );\n\n // Ensure that the condition \"game just started\" (or ended) is valid only for one frame.\n gdjs.registerRuntimeScenePostEventsCallback(() => {\n if (disableMultiplayerForTesting) return;\n\n _hasLobbyGameJustStarted = false;\n _hasLobbyGameJustEnded = false;\n });\n\n const getLobbiesWindowUrl = ({\n runtimeGame,\n gameId,\n }: {\n runtimeGame: gdjs.RuntimeGame;\n gameId: string;\n }) => {\n // Uncomment to test the case of a failing loading:\n // return 'https://gd.games.wronglink';\n\n const baseUrl = 'https://gd.games';\n // Uncomment to test locally:\n // const baseUrl = 'http://localhost:4000';\n\n const url = new URL(\n `${baseUrl}/games/${gameId}/lobbies${_lobbyId ? `/${_lobbyId}` : ''}`\n );\n url.searchParams.set(\n 'gameVersion',\n runtimeGame.getGameData().properties.version\n );\n if (runtimeGame.getAdditionalOptions().nativeMobileApp) {\n url.searchParams.set('nativeMobileApp', 'true');\n }\n url.searchParams.set(\n 'isPreview',\n runtimeGame.isPreview() ? 'true' : 'false'\n );\n if (isUsingGDevelopDevelopmentEnvironment) {\n url.searchParams.set('dev', 'true');\n }\n if (_connectionId) {\n url.searchParams.set('connectionId', _connectionId);\n }\n if (playerNumber) {\n url.searchParams.set('positionInLobby', playerNumber.toString());\n }\n const playerId = gdjs.playerAuthentication.getUserId();\n if (playerId) {\n url.searchParams.set('playerId', playerId);\n }\n const playerToken = gdjs.playerAuthentication.getUserToken();\n if (playerToken) {\n url.searchParams.set('playerToken', playerToken);\n }\n // Increment this value when a new feature is introduced so we can\n // adapt the interface of the lobbies.\n url.searchParams.set('multiplayerVersion', '2');\n\n return url.toString();\n };\n\n export const setObjectsSynchronizationRate = (rate: number) => {\n if (rate < 1 || rate > 60) {\n logger.warn(\n `Invalid rate ${rate} for object synchronization. Defaulting to ${DEFAULT_OBJECT_MAX_SYNC_RATE}.`\n );\n _objectMaxSyncRate = DEFAULT_OBJECT_MAX_SYNC_RATE;\n } else {\n _objectMaxSyncRate = rate;\n }\n };\n\n export const getObjectsSynchronizationRate = () => _objectMaxSyncRate;\n\n /**\n * Returns true if the game has just started,\n * useful to switch to the game scene.\n */\n export const hasLobbyGameJustStarted = () => _hasLobbyGameJustStarted;\n\n export const isLobbyGameRunning = () => _isLobbyGameRunning;\n\n export const isReadyToSendOrReceiveGameUpdateMessages = () =>\n _isReadyToSendOrReceiveGameUpdateMessages;\n\n /**\n * Returns true if the game has just ended,\n * useful to switch back to to the main menu.\n */\n export const hasLobbyGameJustEnded = () => _hasLobbyGameJustEnded;\n\n /**\n * Returns the number of players in the lobby.\n */\n export const getPlayersInLobbyCount = (): number => {\n // Whether the lobby game has started or not, the number of players in the lobby\n // is the number of connected players.\n return gdjs.multiplayerMessageManager.getNumberOfConnectedPlayers();\n };\n\n /**\n * Returns true if the player at this position is connected to the lobby.\n */\n export const isPlayerConnected = (playerNumber: number): boolean => {\n return gdjs.multiplayerMessageManager.isPlayerConnected(playerNumber);\n };\n\n /**\n * Returns the position of the current player in the lobby.\n * Return 0 if the player is not in the lobby.\n * Returns 1, 2, 3, ... if the player is in the lobby.\n */\n export const getCurrentPlayerNumber = (): number => {\n return playerNumber || 0;\n };\n\n /**\n * Returns true if the player is the host in the lobby.\n * This can change during the game.\n */\n export const isCurrentPlayerHost = (): boolean => {\n return (\n !!hostPeerId &&\n hostPeerId === gdjs.multiplayerPeerJsHelper.getCurrentId()\n );\n };\n\n /**\n * Returns true if the host left and the game is either:\n * - picking a new host\n * - waiting for everyone to connect to the new host\n */\n export const isMigratingHost = (): boolean => {\n return !!_isChangingHost;\n };\n\n /**\n * If this is set, instead of migrating the host, the lobby will end when the host leaves.\n */\n export const endLobbyWhenHostLeaves = (enable: boolean) => {\n _shouldEndLobbyWhenHostLeaves = enable;\n };\n\n export const shouldEndLobbyWhenHostLeaves = () =>\n _shouldEndLobbyWhenHostLeaves;\n\n /**\n * Returns the player username at the given number in the lobby.\n * The number is shifted by one, so that the first player has number 1.\n */\n export const getPlayerUsername = (playerNumber: number): string => {\n return gdjs.multiplayerMessageManager.getPlayerUsername(playerNumber);\n };\n\n /**\n * Returns the player username of the current player in the lobby.\n */\n export const getCurrentPlayerUsername = (): string => {\n const currentPlayerNumber = getCurrentPlayerNumber();\n return getPlayerUsername(currentPlayerNumber);\n };\n\n const handleLeavingPlayer = (runtimeScene: gdjs.RuntimeScene) => {\n const lastestPlayerWhoJustLeft = gdjs.multiplayerMessageManager.getLatestPlayerWhoJustLeft();\n if (lastestPlayerWhoJustLeft) {\n const playerUsername = getPlayerUsername(lastestPlayerWhoJustLeft);\n gdjs.multiplayerComponents.displayPlayerLeftNotification(\n runtimeScene,\n playerUsername\n );\n // We remove the players who just left 1 by 1, so that they can be treated in different frames.\n // This is especially important if the expression to know the latest player who just left is used,\n // to avoid missing a player leaving.\n gdjs.multiplayerMessageManager.removePlayerWhoJustLeft();\n\n // When a player leaves, we send a heartbeat to the backend so that they're aware of the players in the lobby.\n // Do not await as we want don't want to block the execution of the of the rest of the logic.\n if (\n isCurrentPlayerHost() &&\n isReadyToSendOrReceiveGameUpdateMessages()\n ) {\n sendHeartbeatToBackend();\n }\n }\n };\n\n const handleJoiningPlayer = (runtimeScene: gdjs.RuntimeScene) => {\n const lastestPlayerWhoJustJoined = gdjs.multiplayerMessageManager.getLatestPlayerWhoJustJoined();\n if (lastestPlayerWhoJustJoined) {\n const playerUsername = getPlayerUsername(lastestPlayerWhoJustJoined);\n gdjs.multiplayerComponents.displayPlayerJoinedNotification(\n runtimeScene,\n playerUsername\n );\n\n // We also send a heartbeat to the backend right away, so that they're aware of the players in the lobby.\n // Do not await as we want don't want to block the execution of the of the rest of the logic.\n if (\n isCurrentPlayerHost() &&\n isReadyToSendOrReceiveGameUpdateMessages()\n ) {\n sendHeartbeatToBackend();\n }\n }\n // We remove the players who just joined 1 by 1, so that they can be treated in different frames.\n // This is especially important if the expression to know the latest player who just joined is used,\n // to avoid missing a player joining.\n gdjs.multiplayerMessageManager.removePlayerWhoJustJoined();\n };\n\n /**\n * Returns true if the game is registered, false otherwise.\n * Useful to display a message to the user to register the game before logging in.\n */\n const checkIfGameIsRegistered = (\n runtimeGame: gdjs.RuntimeGame,\n gameId: string,\n tries: number = 0\n ): Promise<boolean> => {\n const rootApi = isUsingGDevelopDevelopmentEnvironment\n ? 'https://api-dev.gdevelop.io'\n : 'https://api.gdevelop.io';\n const url = `${rootApi}/game/public-game/${gameId}`;\n return fetch(url, { method: 'HEAD' }).then(\n (response) => {\n if (response.status !== 200) {\n logger.warn(\n `Error while fetching the game: ${response.status} ${response.statusText}`\n );\n\n // If the response is not 404, it may be a timeout, so retry a few times.\n if (response.status === 404 || tries > 2) {\n return false;\n }\n\n return checkIfGameIsRegistered(runtimeGame, gameId, tries + 1);\n }\n return true;\n },\n (err) => {\n logger.error('Error while fetching game:', err);\n return false;\n }\n );\n };\n\n const handleJoinLobbyEvent = function (\n runtimeScene: gdjs.RuntimeScene,\n lobbyId: string\n ) {\n if (_connectionId) {\n logger.info('Already connected to a lobby.');\n return;\n }\n\n if (_websocket) {\n logger.warn('Already connected to a lobby. Closing the previous one.');\n _websocket.close();\n _connectionId = null;\n playerNumber = null;\n hostPeerId = null;\n _lobbyId = null;\n _websocket = null;\n }\n\n const gameId = gdjs.projectData.properties.projectUuid;\n const playerId = gdjs.playerAuthentication.getUserId();\n const playerToken = gdjs.playerAuthentication.getUserToken();\n if (!gameId) {\n logger.error('Cannot open lobbies if the project has no ID.');\n return;\n }\n if (!playerId || !playerToken) {\n logger.warn('Cannot open lobbies if the player is not connected.');\n return;\n }\n const wsPlayApi = isUsingGDevelopDevelopmentEnvironment\n ? 'wss://api-ws-dev.gdevelop.io/play'\n : 'wss://api-ws.gdevelop.io/play';\n\n const wsUrl = new URL(wsPlayApi);\n wsUrl.searchParams.set('gameId', gameId);\n wsUrl.searchParams.set('lobbyId', lobbyId);\n wsUrl.searchParams.set('playerId', playerId);\n wsUrl.searchParams.set('connectionType', 'lobby');\n wsUrl.searchParams.set('playerGameToken', playerToken);\n _websocket = new WebSocket(wsUrl.toString());\n _websocket.onopen = () => {\n logger.info('Connected to the lobby.');\n // Register a heartbeat to keep the connection alive.\n _websocketHeartbeatIntervalFunction = setInterval(() => {\n if (_websocket) {\n _websocket.send(\n JSON.stringify({\n action: 'heartbeat',\n connectionType: 'lobby',\n })\n );\n }\n }, DEFAULT_WEBSOCKET_HEARTBEAT_INTERVAL);\n\n // When socket is open, ask for the connectionId and send more session info, so that we can inform the lobbies window.\n if (_websocket) {\n _websocket.send(JSON.stringify({ action: 'getConnectionId' }));\n const platformInfo = runtimeScene.getGame().getPlatformInfo();\n _websocket.send(\n JSON.stringify({\n action: 'sessionInformation',\n connectionType: 'lobby',\n isCordova: platformInfo.isCordova,\n devicePlatform: platformInfo.devicePlatform,\n navigatorPlatform: platformInfo.navigatorPlatform,\n hasTouch: platformInfo.hasTouch,\n supportedCompressionMethods:\n platformInfo.supportedCompressionMethods,\n })\n );\n }\n };\n _websocket.onmessage = (event) => {\n if (event.data) {\n const messageContent = JSON.parse(event.data);\n switch (messageContent.type) {\n case 'connectionId': {\n const messageData = messageContent.data;\n const connectionId = messageData.connectionId;\n const positionInLobby = messageData.positionInLobby;\n const validIceServers = messageData.validIceServers || [];\n const brokerServerConfig = messageData.brokerServerConfig;\n\n if (!connectionId || !positionInLobby) {\n logger.error('No connectionId or position received');\n gdjs.multiplayerComponents.displayErrorNotification(\n runtimeScene\n );\n // Close the websocket as something wrong happened.\n if (_websocket) _websocket.close();\n return;\n }\n\n handleConnectionIdReceived({\n runtimeScene,\n connectionId,\n positionInLobby,\n lobbyId,\n playerId,\n playerToken,\n validIceServers,\n brokerServerConfig,\n });\n break;\n }\n case 'lobbyUpdated': {\n const messageData = messageContent.data;\n const positionInLobby = messageData.positionInLobby;\n handleLobbyUpdatedEvent({\n runtimeScene,\n positionInLobby,\n });\n break;\n }\n case 'gameCountdownStarted': {\n const messageData = messageContent.data;\n const compressionMethod = messageData.compressionMethod || 'none';\n handleGameCountdownStartedEvent({\n runtimeScene,\n compressionMethod,\n });\n break;\n }\n case 'gameStarted': {\n const messageData = messageContent.data;\n currentLobbyHeartbeatInterval =\n messageData.heartbeatInterval ||\n DEFAULT_LOBBY_HEARTBEAT_INTERVAL;\n\n handleGameStartedEvent({\n runtimeScene,\n });\n break;\n }\n case 'peerId': {\n const messageData = messageContent.data;\n if (!messageData) {\n logger.error('No message received');\n return;\n }\n const peerId = messageData.peerId;\n const compressionMethod = messageData.compressionMethod;\n if (!peerId || !compressionMethod) {\n logger.error('Malformed message received');\n return;\n }\n\n handlePeerIdEvent({ peerId, compressionMethod });\n break;\n }\n }\n }\n };\n _websocket.onclose = () => {\n if (!_isLobbyGameRunning) {\n logger.info('Disconnected from the lobby.');\n }\n\n _connectionId = null;\n _websocket = null;\n if (_websocketHeartbeatIntervalFunction) {\n clearInterval(_websocketHeartbeatIntervalFunction);\n }\n\n // If the game is running, then all good.\n // Otherwise, the player left the lobby.\n if (_isLobbyGameRunning) {\n return;\n }\n\n const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(\n runtimeScene\n );\n\n if (!lobbiesIframe || !lobbiesIframe.contentWindow) {\n return;\n }\n\n // Tell the Lobbies iframe that the lobby has been left.\n lobbiesIframe.contentWindow.postMessage(\n {\n id: 'lobbyLeft',\n },\n '*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.\n );\n };\n };\n\n const handleConnectionIdReceived = function ({\n runtimeScene,\n connectionId,\n positionInLobby,\n lobbyId,\n playerId,\n playerToken,\n validIceServers,\n brokerServerConfig,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n connectionId: string;\n positionInLobby: number;\n lobbyId: string;\n playerId: string;\n playerToken: string;\n validIceServers: {\n urls: string;\n username?: string;\n credential?: string;\n }[];\n brokerServerConfig?: {\n hostname: string;\n port: number;\n path: string;\n key: string;\n secure: boolean;\n };\n }) {\n // When the connectionId is received, initialise PeerJS so players can connect to each others afterwards.\n if (validIceServers.length) {\n for (const server of validIceServers) {\n gdjs.multiplayerPeerJsHelper.useCustomICECandidate(\n server.urls,\n server.username,\n server.credential\n );\n }\n }\n if (brokerServerConfig) {\n gdjs.multiplayerPeerJsHelper.useCustomBrokerServer(\n brokerServerConfig.hostname,\n brokerServerConfig.port,\n brokerServerConfig.path,\n brokerServerConfig.key,\n brokerServerConfig.secure\n );\n } else {\n gdjs.multiplayerPeerJsHelper.useDefaultBrokerServer();\n }\n\n _connectionId = connectionId;\n playerNumber = positionInLobby;\n // We save the lobbyId here as this is the moment when the player is really connected to the lobby.\n _lobbyId = lobbyId;\n\n // Then we inform the lobbies window that the player has joined.\n const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(\n runtimeScene\n );\n\n if (!lobbiesIframe || !lobbiesIframe.contentWindow) {\n logger.error(\n 'The lobbies iframe is not opened, cannot send the join message.'\n );\n return;\n }\n\n lobbiesIframe.contentWindow.postMessage(\n {\n id: 'lobbyJoined',\n lobbyId,\n playerId,\n playerToken,\n connectionId: _connectionId,\n positionInLobby,\n },\n // Specify the origin to avoid leaking the playerToken.\n // Replace with '*' to test locally.\n 'https://gd.games'\n // '*'\n );\n };\n\n const handleLeaveLobbyEvent = function () {\n if (_websocket) {\n _websocket.close();\n }\n _connectionId = null;\n playerNumber = null;\n hostPeerId = null;\n _lobbyId = null;\n _websocket = null;\n };\n\n const handleLobbyUpdatedEvent = function ({\n runtimeScene,\n positionInLobby,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n positionInLobby: number;\n }) {\n // This is mainly useful when joining a lobby, or when the lobby is updated before the game starts.\n // The position in lobby should never change after the game has started (the WS is closed anyway).\n playerNumber = positionInLobby;\n\n // If the player is in the lobby, tell the lobbies window that the lobby has been updated,\n // as well as the player position.\n const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(\n runtimeScene\n );\n\n if (!lobbiesIframe || !lobbiesIframe.contentWindow) {\n return;\n }\n\n lobbiesIframe.contentWindow.postMessage(\n {\n id: 'lobbyUpdated',\n positionInLobby,\n },\n '*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.\n );\n };\n\n const handleGameCountdownStartedEvent = function ({\n runtimeScene,\n compressionMethod,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n compressionMethod: gdjs.multiplayerPeerJsHelper.CompressionMethod;\n }) {\n gdjs.multiplayerPeerJsHelper.setCompressionMethod(compressionMethod);\n\n // When the countdown starts, if we are player number 1, we are chosen as the host.\n // We then send the peerId to others so they can connect via P2P.\n if (getCurrentPlayerNumber() === 1) {\n sendPeerId();\n }\n\n // Just pass along the message to the iframe so that it can display the countdown.\n const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(\n runtimeScene\n );\n\n if (!lobbiesIframe || !lobbiesIframe.contentWindow) {\n logger.info('The lobbies iframe is not opened, not sending message.');\n return;\n }\n\n lobbiesIframe.contentWindow.postMessage(\n {\n id: 'gameCountdownStarted',\n },\n '*' // We could restrict to GDevelop games platform but it's not necessary as the message is not sensitive, and it allows easy debugging.\n );\n\n // Prevent the player from leaving the lobby while the game is starting.\n gdjs.multiplayerComponents.hideLobbiesCloseButtonTemporarily(\n runtimeScene\n );\n };\n\n const sendHeartbeatToBackend = async function () {\n const gameId = gdjs.projectData.properties.projectUuid;\n if (!gameId || !_lobbyId) {\n logger.error(\n 'Cannot keep the lobby playing without the game ID or lobby ID.'\n );\n return;\n }\n\n const heartbeatRelativeUrl = `/play/game/${gameId}/public-lobby/${_lobbyId}/action/heartbeat`;\n const players = gdjs.multiplayerMessageManager.getConnectedPlayers();\n try {\n await fetchAsPlayer({\n relativeUrl: heartbeatRelativeUrl,\n method: 'POST',\n body: JSON.stringify({\n players,\n }),\n dev: isUsingGDevelopDevelopmentEnvironment,\n });\n } catch (error) {\n logger.error('Error while sending heartbeat, retrying:', error);\n try {\n await fetchAsPlayer({\n relativeUrl: heartbeatRelativeUrl,\n method: 'POST',\n body: JSON.stringify({\n players,\n }),\n dev: isUsingGDevelopDevelopmentEnvironment,\n });\n } catch (error) {\n logger.error(\n 'Error while sending heartbeat a second time. Giving up:',\n error\n );\n }\n }\n };\n\n /**\n * When the game receives the information that the game has started, close the\n * lobbies window, focus on the game, and set the flag to true.\n */\n const handleGameStartedEvent = function ({\n runtimeScene,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n }) {\n // It is possible the connection to other players didn't work.\n // If that's the case, show an error message and leave the lobby.\n // If we are the host, still start the game, as this allows a player to test the game alone.\n const allConnectedPeers = gdjs.multiplayerPeerJsHelper.getAllPeers();\n if (!isCurrentPlayerHost() && allConnectedPeers.length === 0) {\n gdjs.multiplayerComponents.displayConnectionErrorNotification(\n runtimeScene\n );\n // Do as if the player left the lobby.\n handleLeaveLobbyEvent();\n removeLobbiesContainer(runtimeScene);\n focusOnGame(runtimeScene);\n return;\n }\n\n // If we are the host, start pinging the backend to let it know the lobby is running.\n if (isCurrentPlayerHost()) {\n _lobbyHeartbeatIntervalFunction = setInterval(async () => {\n await sendHeartbeatToBackend();\n }, currentLobbyHeartbeatInterval);\n }\n\n // If we are connected to players, then the game can start.\n logger.info('Lobby game has started.');\n // In case we're joining an existing lobby, read the saved messages to catch-up with the game state.\n gdjs.multiplayerMessageManager.handleSavedUpdateMessages(runtimeScene);\n _isReadyToSendOrReceiveGameUpdateMessages = true;\n _hasLobbyGameJustStarted = true;\n _isLobbyGameRunning = true;\n removeLobbiesContainer(runtimeScene);\n // Close the websocket, as we don't need it anymore.\n if (_websocket) {\n _websocket.close();\n }\n focusOnGame(runtimeScene);\n };\n\n /**\n * When the game receives the information that the game has ended, set the flag to true,\n * so that the game can switch back to the main menu for instance.\n */\n export const handleLobbyGameEnded = function () {\n logger.info('Lobby game has ended.');\n _hasLobbyGameJustEnded = true;\n _isLobbyGameRunning = false;\n _lobbyId = null;\n playerNumber = null;\n hostPeerId = null;\n _isReadyToSendOrReceiveGameUpdateMessages = false;\n if (_lobbyHeartbeatIntervalFunction) {\n clearInterval(_lobbyHeartbeatIntervalFunction);\n _lobbyHeartbeatIntervalFunction = null;\n }\n\n // Disconnect from any P2P connections.\n gdjs.multiplayerPeerJsHelper.disconnectFromAllPeers();\n\n // Clear the expected acknowledgments, as the game is ending.\n gdjs.multiplayerMessageManager.clearAllMessagesTempData();\n };\n\n /**\n * When the game receives the information of the peerId, then\n * the player can connect to the peer.\n */\n const handlePeerIdEvent = function ({\n peerId,\n compressionMethod,\n }: {\n peerId: string;\n compressionMethod: gdjs.multiplayerPeerJsHelper.CompressionMethod;\n }) {\n // When a peerId is received, trigger a P2P connection with the peer, just after setting the compression method.\n gdjs.multiplayerPeerJsHelper.setCompressionMethod(compressionMethod);\n const currentPeerId = gdjs.multiplayerPeerJsHelper.getCurrentId();\n if (!currentPeerId) {\n logger.error(\n 'No peerId found, the player does not seem connected to the broker server.'\n );\n return;\n }\n\n if (currentPeerId === peerId) {\n logger.info('Received our own peerId, ignoring.');\n return;\n }\n\n hostPeerId = peerId;\n gdjs.multiplayerPeerJsHelper.connect(peerId);\n };\n\n /**\n * When the game receives a start countdown message from the lobby, just send it to all\n * players in the lobby via the websocket.\n * It will then trigger an event from the websocket to all players in the lobby.\n */\n const handleStartGameCountdownMessage = function () {\n if (!_websocket) {\n logger.error(\n 'No connection to send the start countdown message. Are you connected to a lobby?'\n );\n return;\n }\n\n _websocket.send(\n JSON.stringify({\n action: 'startGameCountdown',\n connectionType: 'lobby',\n })\n );\n };\n\n /**\n * When the game receives a start game message from the lobby, just send it to all\n * players in the lobby via the websocket.\n * It will then trigger an event from the websocket to all players in the lobby.\n */\n const handleStartGameMessage = function () {\n if (!_websocket) {\n logger.error(\n 'No connection to send the start countdown message. Are you connected to a lobby?'\n );\n return;\n }\n\n _websocket.send(\n JSON.stringify({\n action: 'startGame',\n connectionType: 'lobby',\n })\n );\n\n // As the host, start sending messages to the players.\n _isReadyToSendOrReceiveGameUpdateMessages = true;\n };\n\n /**\n * When the game receives a join game message from the lobby, send it via the WS\n * waiting for a peerId to be received and that the connection happens automatically.\n */\n const handleJoinGameMessage = function () {\n if (!_websocket) {\n logger.error(\n 'No connection to send the start countdown message. Are you connected to a lobby?'\n );\n return;\n }\n\n _websocket.send(\n JSON.stringify({\n action: 'joinGame',\n connectionType: 'lobby',\n })\n );\n };\n\n /**\n * When the first heartbeat is received, we consider the connection to the host as working,\n * we inform the backend services that the connection is ready, so it can start the game when\n * everyone is ready.\n */\n export const markConnectionAsConnected = function () {\n if (!_websocket) {\n return;\n }\n\n _websocket.send(\n JSON.stringify({\n action: 'updateConnection',\n connectionType: 'lobby',\n status: 'connected',\n peerId: gdjs.multiplayerPeerJsHelper.getCurrentId(),\n })\n );\n };\n\n const clearChangeHostRequestData = function (\n runtimeScene: gdjs.RuntimeScene\n ) {\n _lobbyChangeHostRequest = null;\n _lobbyChangeHostRequestInitiatedAt = null;\n _lobbyNewHostPickedAt = null;\n if (_resumeTimeout) {\n clearTimeout(_resumeTimeout);\n _resumeTimeout = null;\n }\n _isChangingHost = false;\n if (hostPeerId) {\n gdjs.multiplayerComponents.showHostMigrationFinishedNotification(\n runtimeScene\n );\n } else {\n gdjs.multiplayerComponents.showHostMigrationFailedNotification(\n runtimeScene\n );\n }\n };\n\n export const resumeGame = async function (runtimeScene: gdjs.RuntimeScene) {\n if (isCurrentPlayerHost()) {\n // Send message to other players to indicate the game is resuming.\n gdjs.multiplayerMessageManager.sendResumeGameMessage();\n\n // Start sending heartbeats to the backend.\n await sendHeartbeatToBackend();\n _lobbyHeartbeatIntervalFunction = setInterval(async () => {\n await sendHeartbeatToBackend();\n }, currentLobbyHeartbeatInterval);\n }\n\n // Migration is finished.\n clearChangeHostRequestData(runtimeScene);\n };\n\n /**\n * When a host is being changed, multiple cases can happen:\n * - We are the new host and the only one in the lobby. Unpause the game right away.\n * - We are the new host and there are other players in the new lobby. Wait for them to connect:\n * - if they are all connected, unpause the game.\n * - if we reach a timeout, a player may have disconnected at the same time, unpause the game.\n * - We are not the new host. Connect to the new host peerId.\n * - If we cannot connect, leave the lobby.\n * - when we receive a message to unpause the game, unpause it.\n * - if we reach a timeout without the message, leave the lobby, something wrong happened.\n */\n const checkHostChangeRequestRegularly = async function ({\n runtimeScene,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n }) {\n if (!_lobbyChangeHostRequest || !_lobbyChangeHostRequestInitiatedAt) {\n return;\n }\n\n // Refresh the request to get the latest information.\n try {\n const changeHostRelativeUrl = `/play/game/${\n _lobbyChangeHostRequest.gameId\n }/public-lobby/${\n _lobbyChangeHostRequest.lobbyId\n }/lobby-change-host-request?peerId=${gdjs.multiplayerPeerJsHelper.getCurrentId()}`;\n\n const lobbyChangeHostRequest = await fetchAsPlayer({\n relativeUrl: changeHostRelativeUrl,\n method: 'GET',\n dev: isUsingGDevelopDevelopmentEnvironment,\n });\n _lobbyChangeHostRequest = lobbyChangeHostRequest;\n } catch (error) {\n logger.error(\n 'Error while trying to retrieve the lobby change host request:',\n error\n );\n handleLobbyGameEnded();\n clearChangeHostRequestData(runtimeScene);\n return;\n }\n\n if (!_lobbyChangeHostRequest) {\n throw new Error('No lobby change host request received.');\n }\n\n const newHostPeerId = _lobbyChangeHostRequest.newHostPeerId;\n if (!newHostPeerId) {\n logger.info('No new host picked yet.');\n if (\n getTimeNow() - _lobbyChangeHostRequestInitiatedAt >\n DEFAULT_LOBBY_CHANGE_HOST_REQUEST_TIMEOUT\n ) {\n logger.error(\n 'Timeout while waiting for the lobby host change. Giving up.'\n );\n handleLobbyGameEnded();\n clearChangeHostRequestData(runtimeScene);\n return;\n }\n\n logger.info('Retrying...');\n setTimeout(() => {\n checkHostChangeRequestRegularly({ runtimeScene });\n }, DEFAULT_LOBBY_CHANGE_HOST_REQUEST_CHECK_INTERVAL);\n return;\n }\n\n try {\n const newLobbyId = _lobbyChangeHostRequest.newLobbyId;\n const newPlayers = _lobbyChangeHostRequest.newPlayers;\n if (!newLobbyId || !newPlayers) {\n logger.error(\n 'Change host request is incomplete. Cannot change host.'\n );\n handleLobbyGameEnded();\n clearChangeHostRequestData(runtimeScene);\n return;\n }\n hostPeerId = newHostPeerId;\n _lobbyNewHostPickedAt = getTimeNow();\n _lobbyId = newLobbyId;\n\n if (newHostPeerId === gdjs.multiplayerPeerJsHelper.getCurrentId()) {\n logger.info(\n `We are the new host. Switching to lobby ${newLobbyId} and awaiting for ${\n newPlayers.length - 1\n } player(s) to connect.`\n );\n await checkExpectedConnectedPlayersRegularly({\n runtimeScene,\n });\n } else {\n logger.info(\n `Connecting to new host and switching lobby to ${newLobbyId}.`\n );\n gdjs.multiplayerPeerJsHelper.connect(newHostPeerId);\n _resumeTimeout = setTimeout(() => {\n logger.error(\n 'Timeout while waiting for the game to resume. Leaving the lobby.'\n );\n handleLobbyGameEnded();\n clearChangeHostRequestData(runtimeScene);\n }, DEFAULT_LOBBY_EXPECTED_RESUME_TIMEOUT);\n }\n } catch (error) {\n logger.error('Error while trying to change host:', error);\n handleLobbyGameEnded();\n clearChangeHostRequestData(runtimeScene);\n }\n };\n\n /**\n * Helper for the new host, to check if they have all the expected players connected.\n */\n const checkExpectedConnectedPlayersRegularly = async function ({\n runtimeScene,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n }) {\n if (!_lobbyChangeHostRequest) {\n return;\n }\n\n const expectedNewPlayers = _lobbyChangeHostRequest.newPlayers;\n if (!expectedNewPlayers) {\n logger.error('No expected players in the lobby change host request.');\n handleLobbyGameEnded();\n clearChangeHostRequestData(runtimeScene);\n return;\n }\n const expectedNewOtherPlayerNumbers = expectedNewPlayers.map(\n (player) => player.playerNumber\n );\n\n // First look for players who left during the migration.\n const playerNumbersConnectedBeforeMigration = gdjs.multiplayerMessageManager\n .getConnectedPlayers()\n .map((player) => player.playerNumber);\n const playerNumbersWhoLeftDuringMigration = playerNumbersConnectedBeforeMigration.filter(\n (playerNumberBeforeMigration) =>\n !expectedNewOtherPlayerNumbers.includes(playerNumberBeforeMigration)\n );\n playerNumbersWhoLeftDuringMigration.map((playerNumberWhoLeft) => {\n logger.info(\n `Player ${playerNumberWhoLeft} left during the host migration. Marking as disconnected.`\n );\n gdjs.multiplayerMessageManager.markPlayerAsDisconnected({\n runtimeScene,\n playerNumber: playerNumberWhoLeft,\n });\n });\n\n // Then check if all expected players are connected.\n const playerNumbersWhoDidNotConnect = expectedNewOtherPlayerNumbers.filter(\n (otherPlayerNumber) =>\n otherPlayerNumber !== playerNumber && // We don't look for ourselves\n !gdjs.multiplayerMessageManager.hasReceivedHeartbeatFromPlayer(\n otherPlayerNumber\n )\n );\n\n if (playerNumbersWhoDidNotConnect.length === 0) {\n logger.info('All expected players are connected. Resuming the game.');\n await resumeGame(runtimeScene);\n return;\n }\n\n if (\n _lobbyNewHostPickedAt &&\n getTimeNow() - _lobbyNewHostPickedAt >\n DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_TIMEOUT &&\n playerNumbersWhoDidNotConnect.length > 0\n ) {\n logger.error(\n `Timeout while waiting for players ${playerNumbersWhoDidNotConnect.join(\n ', '\n )} to connect. Assume they disconnected.`\n );\n playerNumbersWhoDidNotConnect.map((missingPlayerNumber) => {\n gdjs.multiplayerMessageManager.markPlayerAsDisconnected({\n runtimeScene,\n playerNumber: missingPlayerNumber,\n });\n });\n await resumeGame(runtimeScene);\n return;\n }\n\n setTimeout(() => {\n checkExpectedConnectedPlayersRegularly({\n runtimeScene,\n });\n }, DEFAULT_LOBBY_EXPECTED_CONNECTED_PLAYERS_CHECK_INTERVAL);\n };\n\n /**\n * When the host disconnects, we inform the backend we lost the connection and we need a new lobby/host.\n */\n export const handleHostDisconnected = async function ({\n runtimeScene,\n }: {\n runtimeScene: gdjs.RuntimeScene;\n }) {\n if (!_isLobbyGameRunning) {\n // This can happen when the game ends. Nothing to do here.\n return;\n }\n\n if (_lobbyChangeHostRequest) {\n // The new host disconnected while we are already changing host.\n // Let's end the lobby game to avoid weird situations.\n handleLobbyGameEnded();\n clearChangeHostRequestData(runtimeScene);\n }\n\n const gameId = gdjs.projectData.properties.projectUuid;\n\n if (!gameId || !_lobbyId) {\n logger.error(\n 'Cannot ask for a host change without the game ID or lobby ID.'\n );\n return;\n }\n\n try {\n _isChangingHost = true;\n gdjs.multiplayerComponents.displayHostMigrationNotification(\n runtimeScene\n );\n\n const changeHostRelativeUrl = `/play/game/${gameId}/public-lobby/${_lobbyId}/lobby-change-host-request`;\n const playersInfo = gdjs.multiplayerMessageManager.getPlayersInfo();\n const playersInfoForHostChange = Object.keys(playersInfo).map(\n (playerNumber) => {\n return {\n playerNumber: parseInt(playerNumber, 10),\n playerId: playersInfo[playerNumber].playerId,\n ping: playersInfo[playerNumber].ping,\n };\n }\n );\n const body = JSON.stringify({\n playersInfo: playersInfoForHostChange,\n peerId: gdjs.multiplayerPeerJsHelper.getCurrentId(),\n });\n const lobbyChangeHostRequest = await fetchAsPlayer({\n relativeUrl: changeHostRelativeUrl,\n method: 'POST',\n body,\n dev: isUsingGDevelopDevelopmentEnvironment,\n });\n\n _lobbyChangeHostRequest = lobbyChangeHostRequest;\n _lobbyChangeHostRequestInitiatedAt = getTimeNow();\n\n await checkHostChangeRequestRegularly({ runtimeScene });\n } catch (error) {\n logger.error('Error while trying to change host:', error);\n handleLobbyGameEnded();\n clearChangeHostRequestData(runtimeScene);\n }\n };\n\n /**\n * Action to end the lobby game.\n * This will update the lobby status and inform everyone in the lobby that the game has ended.\n */\n export const endLobbyGame = async function () {\n if (!isLobbyGameRunning()) {\n return;\n }\n\n if (!isCurrentPlayerHost()) {\n logger.error('Only the host can end the game.');\n return;\n }\n\n // Consider the game is ended, so that we don't listen to other players disconnecting.\n _isLobbyGameRunning = false;\n\n logger.info('Ending the lobby game.');\n\n // Inform the players that the game has ended.\n gdjs.multiplayerMessageManager.sendEndGameMessage();\n\n // Also call backend to end the game.\n const gameId = gdjs.projectData.properties.projectUuid;\n if (!gameId || !_lobbyId) {\n logger.error('Cannot end the lobby without the game ID or lobby ID.');\n return;\n }\n\n const endGameRelativeUrl = `/play/game/${gameId}/public-lobby/${_lobbyId}/action/end`;\n try {\n await fetchAsPlayer({\n relativeUrl: endGameRelativeUrl,\n method: 'POST',\n body: JSON.stringify({}),\n dev: isUsingGDevelopDevelopmentEnvironment,\n });\n } catch (error) {\n logger.error('Error while ending the game:', error);\n }\n\n // Do as if everyone left the lobby.\n handleLobbyGameEnded();\n };\n\n /**\n * Helper to send the ID from PeerJS to the lobby players.\n */\n const sendPeerId = function () {\n if (!_websocket) {\n logger.error(\n 'No connection to send the message. Are you connected to a lobby?'\n );\n return;\n }\n\n const peerId = gdjs.multiplayerPeerJsHelper.getCurrentId();\n if (!peerId) {\n logger.error(\n \"No peerId found, the player doesn't seem connected to the broker server.\"\n );\n return;\n }\n\n _websocket.send(\n JSON.stringify({\n action: 'sendPeerId',\n connectionType: 'lobby',\n peerId,\n })\n );\n // We are the host.\n hostPeerId = peerId;\n };\n\n /**\n * Reads the event sent by the lobbies window and\n * react accordingly.\n */\n const receiveLobbiesMessage = function (\n runtimeScene: gdjs.RuntimeScene,\n event: MessageEvent,\n { checkOrigin }: { checkOrigin: boolean }\n ) {\n const allowedOrigins = ['https://gd.games', 'http://localhost:4000'];\n\n // Check origin of message.\n if (checkOrigin && !allowedOrigins.includes(event.origin)) {\n // Wrong origin. Return silently.\n return;\n }\n // Check that message is not malformed.\n if (!event.data.id) {\n throw new Error('Malformed message');\n }\n\n // Handle message.\n switch (event.data.id) {\n case 'lobbiesListenerReady': {\n sendSessionInformation(runtimeScene);\n break;\n }\n case 'joinLobby': {\n if (!event.data.lobbyId) {\n throw new Error('Malformed message.');\n }\n\n handleJoinLobbyEvent(runtimeScene, event.data.lobbyId);\n break;\n }\n case 'startGameCountdown': {\n handleStartGameCountdownMessage();\n break;\n }\n case 'startGame': {\n handleStartGameMessage();\n break;\n }\n case 'leaveLobby': {\n handleLeaveLobbyEvent();\n break;\n }\n case 'joinGame': {\n handleJoinGameMessage();\n break;\n }\n }\n };\n\n /**\n * Handle any error that can occur as part of displaying the lobbies.\n */\n const handleLobbiesError = function (\n runtimeScene: gdjs.RuntimeScene,\n message: string\n ) {\n logger.error(message);\n removeLobbiesContainer(runtimeScene);\n focusOnGame(runtimeScene);\n };\n\n const sendSessionInformation = (runtimeScene: gdjs.RuntimeScene) => {\n const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(\n runtimeScene\n );\n if (!lobbiesIframe || !lobbiesIframe.contentWindow) {\n // Cannot send the message if the iframe is not opened.\n return;\n }\n\n const platformInfo = runtimeScene.getGame().getPlatformInfo();\n\n lobbiesIframe.contentWindow.postMessage(\n {\n id: 'sessionInformation',\n isCordova: platformInfo.isCordova,\n devicePlatform: platformInfo.devicePlatform,\n navigatorPlatform: platformInfo.navigatorPlatform,\n hasTouch: platformInfo.hasTouch,\n },\n '*'\n );\n };\n\n /**\n * Helper to handle lobbies iframe.\n * We open an iframe, and listen to messages posted back to the game window.\n */\n const openLobbiesIframe = (\n runtimeScene: gdjs.RuntimeScene,\n gameId: string\n ) => {\n const targetUrl = getLobbiesWindowUrl({\n runtimeGame: runtimeScene.getGame(),\n gameId,\n });\n\n // Listen to messages posted by the lobbies window, so that we can\n // know when they join or leave a lobby.\n _lobbiesMessageCallback = (event: MessageEvent) => {\n receiveLobbiesMessage(runtimeScene, event, {\n checkOrigin: true,\n });\n };\n window.addEventListener('message', _lobbiesMessageCallback, true);\n\n gdjs.multiplayerComponents.displayIframeInsideLobbiesContainer(\n runtimeScene,\n targetUrl\n );\n };\n\n /**\n * Action to display the lobbies window to the user.\n */\n export const openLobbiesWindow = async (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n if (\n isLobbiesWindowOpen(runtimeScene) ||\n gdjs.playerAuthentication.isAuthenticationWindowOpen()\n ) {\n return;\n }\n\n const _gameId = gdjs.projectData.properties.projectUuid;\n if (!_gameId) {\n handleLobbiesError(\n runtimeScene,\n 'The game ID is missing, the lobbies window cannot be opened.'\n );\n return;\n }\n\n if (_isCheckingIfGameIsRegistered || _isWaitingForLogin) {\n // The action is called multiple times, let's prevent that.\n return;\n }\n\n // Create the lobbies container for the player to wait.\n const domElementContainer = runtimeScene\n .getGame()\n .getRenderer()\n .getDomElementContainer();\n if (!domElementContainer) {\n handleLobbiesError(\n runtimeScene,\n \"The div element covering the game couldn't be found, the lobbies window cannot be displayed.\"\n );\n return;\n }\n\n const onLobbiesContainerDismissed = () => {\n removeLobbiesContainer(runtimeScene);\n };\n\n const playerId = gdjs.playerAuthentication.getUserId();\n const playerToken = gdjs.playerAuthentication.getUserToken();\n if (!playerId || !playerToken) {\n _isWaitingForLogin = true;\n const {\n status,\n } = await gdjs.playerAuthentication.openAuthenticationWindow(\n runtimeScene\n ).promise;\n _isWaitingForLogin = false;\n\n if (status === 'logged') {\n openLobbiesWindow(runtimeScene);\n }\n\n return;\n }\n\n gdjs.multiplayerComponents.displayLobbies(\n runtimeScene,\n onLobbiesContainerDismissed\n );\n\n // If the game is registered, open the lobbies window.\n // Otherwise, open the window indicating that the game is not registered.\n if (_isGameRegistered === null) {\n _isCheckingIfGameIsRegistered = true;\n try {\n const isGameRegistered = await checkIfGameIsRegistered(\n runtimeScene.getGame(),\n _gameId\n );\n _isGameRegistered = isGameRegistered;\n } catch (error) {\n _isGameRegistered = false;\n logger.error(\n 'Error while checking if the game is registered:',\n error\n );\n handleLobbiesError(\n runtimeScene,\n 'Error while checking if the game is registered.'\n );\n return;\n } finally {\n _isCheckingIfGameIsRegistered = false;\n }\n }\n const electron = runtimeScene.getGame().getRenderer().getElectron();\n const wikiOpenAction = electron\n ? () =>\n electron.shell.openExternal(\n 'https://wiki.gdevelop.io/gdevelop5/publishing/web'\n )\n : () =>\n window.open(\n 'https://wiki.gdevelop.io/gdevelop5/publishing/web',\n '_blank'\n );\n\n gdjs.multiplayerComponents.addTextsToLoadingContainer(\n runtimeScene,\n _isGameRegistered,\n wikiOpenAction\n );\n\n if (_isGameRegistered) {\n openLobbiesIframe(runtimeScene, _gameId);\n }\n };\n\n /**\n * Condition to check if the window is open, so that the game can be paused in the background.\n */\n export const isLobbiesWindowOpen = function (\n runtimeScene: gdjs.RuntimeScene\n ): boolean {\n const lobbiesRootContainer = gdjs.multiplayerComponents.getLobbiesRootContainer(\n runtimeScene\n );\n return !!lobbiesRootContainer;\n };\n\n export const showLobbiesCloseButton = function (\n runtimeScene: gdjs.RuntimeScene,\n visible: boolean\n ) {\n gdjs.multiplayerComponents.changeLobbiesWindowCloseActionVisibility(\n runtimeScene,\n visible\n );\n };\n\n /**\n * Remove the container displaying the lobbies window and the callback.\n */\n export const removeLobbiesContainer = function (\n runtimeScene: gdjs.RuntimeScene\n ) {\n removeLobbiesCallbacks();\n gdjs.multiplayerComponents.removeLobbiesContainer(runtimeScene);\n };\n\n /*\n * Remove the lobbies callbacks.\n */\n const removeLobbiesCallbacks = function () {\n // Remove the lobbies callbacks.\n if (_lobbiesMessageCallback) {\n window.removeEventListener('message', _lobbiesMessageCallback, true);\n _lobbiesMessageCallback = null;\n }\n };\n\n /**\n * Focus on game canvas to allow user to interact with it.\n */\n const focusOnGame = function (runtimeScene: gdjs.RuntimeScene) {\n const gameCanvas = runtimeScene.getGame().getRenderer().getCanvas();\n if (gameCanvas) gameCanvas.focus();\n };\n\n /**\n * Action to allow the player to leave the lobby in-game.\n */\n export const leaveGameLobby = async () => {\n // Handle the case where the game has not started yet, so the player is in the lobby.\n handleLeaveLobbyEvent();\n // Handle the case where the game has started, so the player is in the game and connected to other players.\n handleLobbyGameEnded();\n };\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
var gdjs;(function(h){class d{constructor(e,
|
|
1
|
+
var gdjs;(function(h){class d{constructor(e,o,t,r){this._wasRendered=!1;this._textureWidth=0;this._textureHeight=0;this._object=e;const s=o.getGame().getImageManager().getPIXITexture(t),i=r?PIXI.TilingSprite:PIXI.Sprite;this._spritesContainer=new PIXI.Container,this._wrapperContainer=new PIXI.Container,this._centerSprite=new i(new PIXI.Texture(s.baseTexture)),this._borderSprites=[new i(new PIXI.Texture(s.baseTexture)),new PIXI.Sprite(new PIXI.Texture(s.baseTexture)),new i(new PIXI.Texture(s.baseTexture)),new PIXI.Sprite(new PIXI.Texture(s.baseTexture)),new i(new PIXI.Texture(s.baseTexture)),new PIXI.Sprite(new PIXI.Texture(s.baseTexture)),new i(new PIXI.Texture(s.baseTexture)),new PIXI.Sprite(new PIXI.Texture(s.baseTexture))],this.setTexture(t,o),this._spritesContainer.removeChildren(),this._spritesContainer.addChild(this._centerSprite);for(let n=0;n<this._borderSprites.length;++n)this._spritesContainer.addChild(this._borderSprites[n]);this._wrapperContainer.addChild(this._spritesContainer),o.getLayer("").getRenderer().addRendererObject(this._wrapperContainer,e.getZOrder())}getRendererObject(){return this._wrapperContainer}ensureUpToDate(){this._spritesContainer.visible&&this._wasRendered&&this._centerSprite.texture.baseTexture.scaleMode!==PIXI.SCALE_MODES.NEAREST&&(this._spritesContainer.cacheAsBitmap=!0),this._wasRendered=!0}updateOpacity(){this._wrapperContainer.alpha=this._object.opacity/255,this._spritesContainer.cacheAsBitmap=!1}updateAngle(){this._wrapperContainer.rotation=h.toRad(this._object.angle)}updatePosition(){this._wrapperContainer.position.x=this._object.x+this._object._width/2,this._wrapperContainer.position.y=this._object.y+this._object._height/2}_updateLocalPositions(){const e=this._object;this._centerSprite.position.x=e._lBorder,this._centerSprite.position.y=e._tBorder,this._borderSprites[0].position.x=e._width-e._rBorder,this._borderSprites[0].position.y=e._tBorder,this._borderSprites[1].position.x=e._width-this._borderSprites[1].width,this._borderSprites[1].position.y=0,this._borderSprites[2].position.x=e._lBorder,this._borderSprites[2].position.y=0,this._borderSprites[3].position.x=0,this._borderSprites[3].position.y=0,this._borderSprites[4].position.x=0,this._borderSprites[4].position.y=e._tBorder,this._borderSprites[5].position.x=0,this._borderSprites[5].position.y=e._height-this._borderSprites[5].height,this._borderSprites[6].position.x=e._lBorder,this._borderSprites[6].position.y=e._height-e._bBorder,this._borderSprites[7].position.x=e._width-this._borderSprites[7].width,this._borderSprites[7].position.y=e._height-this._borderSprites[7].height}_updateSpritesAndTexturesSize(){const e=this._object;this._centerSprite.width=Math.max(e._width-e._rBorder-e._lBorder,0),this._centerSprite.height=Math.max(e._height-e._tBorder-e._bBorder,0),this._borderSprites[0].width=e._rBorder,this._borderSprites[0].height=Math.max(e._height-e._tBorder-e._bBorder,0),this._borderSprites[2].height=e._tBorder,this._borderSprites[2].width=Math.max(e._width-e._rBorder-e._lBorder,0),this._borderSprites[4].width=e._lBorder,this._borderSprites[4].height=Math.max(e._height-e._tBorder-e._bBorder,0),this._borderSprites[6].height=e._bBorder,this._borderSprites[6].width=Math.max(e._width-e._rBorder-e._lBorder,0),this._wasRendered=!0,this._spritesContainer.cacheAsBitmap=!1}setTexture(e,o){const t=this._object,r=o.getGame().getImageManager().getPIXITexture(e).baseTexture;this._textureWidth=r.width,this._textureHeight=r.height;function s(i){return i.width<0&&(i.width=0),i.height<0&&(i.height=0),i.x<0&&(i.x=0),i.y<0&&(i.y=0),i.x>r.width&&(i.x=r.width),i.y>r.height&&(i.y=r.height),i.x+i.width>r.width&&(i.width=r.width-i.x),i.y+i.height>r.height&&(i.height=r.height-i.y),i}this._centerSprite.texture.destroy(!1),this._centerSprite.texture=new PIXI.Texture(r,s(new PIXI.Rectangle(t._lBorder,t._tBorder,r.width-t._lBorder-t._rBorder,r.height-t._tBorder-t._bBorder))),this._borderSprites[0].texture.destroy(!1),this._borderSprites[0].texture=new PIXI.Texture(r,s(new PIXI.Rectangle(r.width-t._rBorder,t._tBorder,t._rBorder,r.height-t._tBorder-t._bBorder))),this._borderSprites[2].texture.destroy(!1),this._borderSprites[2].texture=new PIXI.Texture(r,s(new PIXI.Rectangle(t._lBorder,0,r.width-t._lBorder-t._rBorder,t._tBorder))),this._borderSprites[4].texture.destroy(!1),this._borderSprites[4].texture=new PIXI.Texture(r,s(new PIXI.Rectangle(0,t._tBorder,t._lBorder,r.height-t._tBorder-t._bBorder))),this._borderSprites[6].texture.destroy(!1),this._borderSprites[6].texture=new PIXI.Texture(r,s(new PIXI.Rectangle(t._lBorder,r.height-t._bBorder,r.width-t._lBorder-t._rBorder,t._bBorder))),this._borderSprites[1].texture.destroy(!1),this._borderSprites[1].texture=new PIXI.Texture(r,s(new PIXI.Rectangle(r.width-t._rBorder,0,t._rBorder,t._tBorder))),this._borderSprites[3].texture.destroy(!1),this._borderSprites[3].texture=new PIXI.Texture(r,s(new PIXI.Rectangle(0,0,t._lBorder,t._tBorder))),this._borderSprites[5].texture.destroy(!1),this._borderSprites[5].texture=new PIXI.Texture(r,s(new PIXI.Rectangle(0,r.height-t._bBorder,t._lBorder,t._bBorder))),this._borderSprites[7].texture.destroy(!1),this._borderSprites[7].texture=new PIXI.Texture(r,s(new PIXI.Rectangle(r.width-t._rBorder,r.height-t._bBorder,t._rBorder,t._bBorder))),this._updateSpritesAndTexturesSize(),this._updateLocalPositions(),this.updatePosition(),this._wrapperContainer.pivot.x=this._object._width/2,this._wrapperContainer.pivot.y=this._object._height/2}updateWidth(){this._wrapperContainer.pivot.x=this._object._width/2,this._updateSpritesAndTexturesSize(),this._updateLocalPositions(),this.updatePosition()}updateHeight(){this._wrapperContainer.pivot.y=this._object._height/2,this._updateSpritesAndTexturesSize(),this._updateLocalPositions(),this.updatePosition()}setColor(e){const o=e.split(";");if(!(o.length<3)){this._centerSprite.tint=h.rgbToHexNumber(parseInt(o[0],10),parseInt(o[1],10),parseInt(o[2],10));for(let t=0;t<this._borderSprites.length;t++)this._borderSprites[t].tint=h.rgbToHexNumber(parseInt(o[0],10),parseInt(o[1],10),parseInt(o[2],10));this._spritesContainer.cacheAsBitmap=!1}}getColor(){const e=new PIXI.Color(this._centerSprite.tint).toRgbArray();return Math.floor(e[0]*255)+";"+Math.floor(e[1]*255)+";"+Math.floor(e[2]*255)}getTextureWidth(){return this._textureWidth}getTextureHeight(){return this._textureHeight}destroy(){for(const e of this._borderSprites)e.destroy({texture:!0});this._centerSprite.destroy({texture:!0}),this._wrapperContainer.destroy(!1),this._spritesContainer.destroy(!1)}}h.PanelSpriteRuntimeObjectRenderer=d})(gdjs||(gdjs={}));
|
|
2
2
|
//# sourceMappingURL=panelspriteruntimeobject-pixi-renderer.js.map
|
package/dist/Runtime/Extensions/PanelSpriteObject/panelspriteruntimeobject-pixi-renderer.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../GDevelop/Extensions/PanelSpriteObject/panelspriteruntimeobject-pixi-renderer.ts"],
|
|
4
|
-
"sourcesContent": ["namespace gdjs {\n class PanelSpriteRuntimeObjectPixiRenderer {\n _object: gdjs.PanelSpriteRuntimeObject;\n /**\n * The _wrapperContainer must be considered as the main container of this object\n * All transformations are applied on this container\n * See updateOpacity for more info why.\n */\n _wrapperContainer: PIXI.Container;\n /**\n * The _spritesContainer is used to create the sprites and apply cacheAsBitmap only.\n */\n _spritesContainer: PIXI.Container;\n _centerSprite: PIXI.Sprite | PIXI.TilingSprite;\n _borderSprites: Array<PIXI.Sprite | PIXI.TilingSprite>;\n _wasRendered: boolean = false;\n _textureWidth = 0;\n _textureHeight = 0;\n\n constructor(\n runtimeObject: gdjs.PanelSpriteRuntimeObject,\n instanceContainer: gdjs.RuntimeInstanceContainer,\n textureName: string,\n tiled: boolean\n ) {\n this._object = runtimeObject;\n const texture = (instanceContainer\n .getGame()\n .getImageManager() as gdjs.PixiImageManager).getPIXITexture(\n textureName\n );\n const StretchedSprite = !tiled ? PIXI.Sprite : PIXI.TilingSprite;\n this._spritesContainer = new PIXI.Container();\n this._wrapperContainer = new PIXI.Container();\n this._centerSprite = new StretchedSprite(\n new PIXI.Texture(texture.baseTexture)\n );\n this._borderSprites = [\n // Right\n new StretchedSprite(new PIXI.Texture(texture.baseTexture)),\n // Top-Right\n new PIXI.Sprite(texture),\n // Top\n new StretchedSprite(new PIXI.Texture(texture.baseTexture)),\n // Top-Left\n new PIXI.Sprite(texture),\n // Left\n new StretchedSprite(new PIXI.Texture(texture.baseTexture)),\n // Bottom-Left\n new PIXI.Sprite(texture),\n // Bottom\n new StretchedSprite(new PIXI.Texture(texture.baseTexture)),\n new PIXI.Sprite(texture),\n ];\n\n //Bottom-Right\n this.setTexture(textureName, instanceContainer);\n this._spritesContainer.removeChildren();\n this._spritesContainer.addChild(this._centerSprite);\n for (let i = 0; i < this._borderSprites.length; ++i) {\n this._spritesContainer.addChild(this._borderSprites[i]);\n }\n this._wrapperContainer.addChild(this._spritesContainer);\n instanceContainer\n .getLayer('')\n .getRenderer()\n .addRendererObject(this._wrapperContainer, runtimeObject.getZOrder());\n }\n\n getRendererObject() {\n return this._wrapperContainer;\n }\n\n ensureUpToDate() {\n if (this._spritesContainer.visible && this._wasRendered) {\n // PIXI uses PIXI.SCALE_MODES.LINEAR for the cached image:\n // this._spritesContainer._cacheData.sprite._texture.baseTexture.scaleMode\n // There seems to be no way to configure this so the optimization is disabled.\n if (\n this._centerSprite.texture.baseTexture.scaleMode !==\n PIXI.SCALE_MODES.NEAREST\n ) {\n // Cache the rendered sprites as a bitmap to speed up rendering when\n // lots of panel sprites are on the scene.\n // Sadly, because of this, we need a wrapper container to workaround\n // a PixiJS issue with alpha (see updateOpacity).\n this._spritesContainer.cacheAsBitmap = true;\n }\n }\n this._wasRendered = true;\n }\n\n updateOpacity(): void {\n // The alpha is updated on a wrapper around the sprite because a known bug\n // in Pixi will create a flicker when cacheAsBitmap is set to true.\n // (see https://github.com/pixijs/pixijs/issues/4610)\n this._wrapperContainer.alpha = this._object.opacity / 255;\n // When the opacity is updated, the cache must be invalidated, otherwise\n // there is a risk of the panel sprite has been cached previously with a\n // different opacity (and cannot be updated anymore).\n this._spritesContainer.cacheAsBitmap = false;\n }\n\n updateAngle(): void {\n this._wrapperContainer.rotation = gdjs.toRad(this._object.angle);\n }\n\n updatePosition(): void {\n this._wrapperContainer.position.x =\n this._object.x + this._object._width / 2;\n this._wrapperContainer.position.y =\n this._object.y + this._object._height / 2;\n }\n\n _updateLocalPositions() {\n const obj = this._object;\n this._centerSprite.position.x = obj._lBorder;\n this._centerSprite.position.y = obj._tBorder;\n\n //Right\n this._borderSprites[0].position.x = obj._width - obj._rBorder;\n this._borderSprites[0].position.y = obj._tBorder;\n\n //Top-right\n this._borderSprites[1].position.x =\n obj._width - this._borderSprites[1].width;\n this._borderSprites[1].position.y = 0;\n\n //Top\n this._borderSprites[2].position.x = obj._lBorder;\n this._borderSprites[2].position.y = 0;\n\n //Top-Left\n this._borderSprites[3].position.x = 0;\n this._borderSprites[3].position.y = 0;\n\n //Left\n this._borderSprites[4].position.x = 0;\n this._borderSprites[4].position.y = obj._tBorder;\n\n //Bottom-Left\n this._borderSprites[5].position.x = 0;\n this._borderSprites[5].position.y =\n obj._height - this._borderSprites[5].height;\n\n //Bottom\n this._borderSprites[6].position.x = obj._lBorder;\n this._borderSprites[6].position.y = obj._height - obj._bBorder;\n\n //Bottom-Right\n this._borderSprites[7].position.x =\n obj._width - this._borderSprites[7].width;\n this._borderSprites[7].position.y =\n obj._height - this._borderSprites[7].height;\n }\n\n _updateSpritesAndTexturesSize() {\n const obj = this._object;\n this._centerSprite.width = Math.max(\n obj._width - obj._rBorder - obj._lBorder,\n 0\n );\n this._centerSprite.height = Math.max(\n obj._height - obj._tBorder - obj._bBorder,\n 0\n );\n\n //Right\n this._borderSprites[0].width = obj._rBorder;\n this._borderSprites[0].height = Math.max(\n obj._height - obj._tBorder - obj._bBorder,\n 0\n );\n\n //Top\n this._borderSprites[2].height = obj._tBorder;\n this._borderSprites[2].width = Math.max(\n obj._width - obj._rBorder - obj._lBorder,\n 0\n );\n\n //Left\n this._borderSprites[4].width = obj._lBorder;\n this._borderSprites[4].height = Math.max(\n obj._height - obj._tBorder - obj._bBorder,\n 0\n );\n\n //Bottom\n this._borderSprites[6].height = obj._bBorder;\n this._borderSprites[6].width = Math.max(\n obj._width - obj._rBorder - obj._lBorder,\n 0\n );\n this._wasRendered = true;\n this._spritesContainer.cacheAsBitmap = false;\n }\n\n setTexture(\n textureName: string,\n instanceContainer: gdjs.RuntimeInstanceContainer\n ): void {\n const obj = this._object;\n const texture = instanceContainer\n .getGame()\n .getImageManager()\n .getPIXITexture(textureName).baseTexture;\n this._textureWidth = texture.width;\n this._textureHeight = texture.height;\n\n function makeInsideTexture(rect) {\n //TODO\n if (rect.width < 0) {\n rect.width = 0;\n }\n if (rect.height < 0) {\n rect.height = 0;\n }\n if (rect.x < 0) {\n rect.x = 0;\n }\n if (rect.y < 0) {\n rect.y = 0;\n }\n if (rect.x > texture.width) {\n rect.x = texture.width;\n }\n if (rect.y > texture.height) {\n rect.y = texture.height;\n }\n if (rect.x + rect.width > texture.width) {\n rect.width = texture.width - rect.x;\n }\n if (rect.y + rect.height > texture.height) {\n rect.height = texture.height - rect.y;\n }\n return rect;\n }\n this._centerSprite.texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n obj._lBorder,\n obj._tBorder,\n texture.width - obj._lBorder - obj._rBorder,\n texture.height - obj._tBorder - obj._bBorder\n )\n )\n );\n\n //Top, Bottom, Right, Left borders:\n this._borderSprites[0].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n texture.width - obj._rBorder,\n obj._tBorder,\n obj._rBorder,\n texture.height - obj._tBorder - obj._bBorder\n )\n )\n );\n this._borderSprites[2].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n obj._lBorder,\n 0,\n texture.width - obj._lBorder - obj._rBorder,\n obj._tBorder\n )\n )\n );\n this._borderSprites[4].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n 0,\n obj._tBorder,\n obj._lBorder,\n texture.height - obj._tBorder - obj._bBorder\n )\n )\n );\n this._borderSprites[6].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n obj._lBorder,\n texture.height - obj._bBorder,\n texture.width - obj._lBorder - obj._rBorder,\n obj._bBorder\n )\n )\n );\n this._borderSprites[1].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n texture.width - obj._rBorder,\n 0,\n obj._rBorder,\n obj._tBorder\n )\n )\n );\n this._borderSprites[3].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(new PIXI.Rectangle(0, 0, obj._lBorder, obj._tBorder))\n );\n this._borderSprites[5].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n 0,\n texture.height - obj._bBorder,\n obj._lBorder,\n obj._bBorder\n )\n )\n );\n this._borderSprites[7].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n texture.width - obj._rBorder,\n texture.height - obj._bBorder,\n obj._rBorder,\n obj._bBorder\n )\n )\n );\n this._updateSpritesAndTexturesSize();\n this._updateLocalPositions();\n this.updatePosition();\n this._wrapperContainer.pivot.x = this._object._width / 2;\n this._wrapperContainer.pivot.y = this._object._height / 2;\n }\n\n updateWidth(): void {\n this._wrapperContainer.pivot.x = this._object._width / 2;\n this._updateSpritesAndTexturesSize();\n this._updateLocalPositions();\n this.updatePosition();\n }\n\n updateHeight(): void {\n this._wrapperContainer.pivot.y = this._object._height / 2;\n this._updateSpritesAndTexturesSize();\n this._updateLocalPositions();\n this.updatePosition();\n }\n\n setColor(rgbColor): void {\n const colors = rgbColor.split(';');\n if (colors.length < 3) {\n return;\n }\n this._centerSprite.tint = gdjs.rgbToHexNumber(\n parseInt(colors[0], 10),\n parseInt(colors[1], 10),\n parseInt(colors[2], 10)\n );\n for (\n let borderCounter = 0;\n borderCounter < this._borderSprites.length;\n borderCounter++\n ) {\n this._borderSprites[borderCounter].tint = gdjs.rgbToHexNumber(\n parseInt(colors[0], 10),\n parseInt(colors[1], 10),\n parseInt(colors[2], 10)\n );\n }\n this._spritesContainer.cacheAsBitmap = false;\n }\n\n getColor() {\n const rgb = new PIXI.Color(this._centerSprite.tint).toRgbArray();\n return (\n Math.floor(rgb[0] * 255) +\n ';' +\n Math.floor(rgb[1] * 255) +\n ';' +\n Math.floor(rgb[2] * 255)\n );\n }\n\n getTextureWidth() {\n return this._textureWidth;\n }\n\n getTextureHeight() {\n return this._textureHeight;\n }\n\n destroy() {\n // Destroy textures because they are instantiated by this class.\n for (const borderSprite of this._borderSprites) {\n borderSprite.destroy({ texture: true });\n }\n this._centerSprite.destroy({ texture: true });\n // Destroy the containers without handling children because they are\n // already handled above.\n this._wrapperContainer.destroy(false);\n this._spritesContainer.destroy(false);\n }\n }\n\n export const PanelSpriteRuntimeObjectRenderer = PanelSpriteRuntimeObjectPixiRenderer;\n export type PanelSpriteRuntimeObjectRenderer = PanelSpriteRuntimeObjectPixiRenderer;\n}\n"],
|
|
5
|
-
"mappings": "AAAA,GAAU,MAAV,UAAU,EAAV,CACE,OAA2C,CAkBzC,YACE,EACA,EACA,EACA,EACA,CATF,kBAAwB,GACxB,mBAAgB,EAChB,oBAAiB,EAQf,KAAK,QAAU,EACf,KAAM,GAAW,EACd,UACA,kBAA4C,eAC7C,GAEI,EAAkB,AAAC,EAAsB,KAAK,aAAnB,KAAK,OACtC,KAAK,kBAAoB,GAAI,MAAK,UAClC,KAAK,kBAAoB,GAAI,MAAK,
|
|
4
|
+
"sourcesContent": ["namespace gdjs {\n class PanelSpriteRuntimeObjectPixiRenderer {\n _object: gdjs.PanelSpriteRuntimeObject;\n /**\n * The _wrapperContainer must be considered as the main container of this object\n * All transformations are applied on this container\n * See updateOpacity for more info why.\n */\n _wrapperContainer: PIXI.Container;\n /**\n * The _spritesContainer is used to create the sprites and apply cacheAsBitmap only.\n */\n _spritesContainer: PIXI.Container;\n _centerSprite: PIXI.Sprite | PIXI.TilingSprite;\n _borderSprites: Array<PIXI.Sprite | PIXI.TilingSprite>;\n _wasRendered: boolean = false;\n _textureWidth = 0;\n _textureHeight = 0;\n\n constructor(\n runtimeObject: gdjs.PanelSpriteRuntimeObject,\n instanceContainer: gdjs.RuntimeInstanceContainer,\n textureName: string,\n tiled: boolean\n ) {\n this._object = runtimeObject;\n const texture = (instanceContainer\n .getGame()\n .getImageManager() as gdjs.PixiImageManager).getPIXITexture(\n textureName\n );\n const StretchedSprite = !tiled ? PIXI.Sprite : PIXI.TilingSprite;\n this._spritesContainer = new PIXI.Container();\n this._wrapperContainer = new PIXI.Container();\n\n // All these textures are going to be replaced in the call to `setTexture`.\n // But to be safe and preserve the invariant that \"these objects own their own\n // textures\", we create a new texture for each sprite.\n this._centerSprite = new StretchedSprite(\n new PIXI.Texture(texture.baseTexture)\n );\n this._borderSprites = [\n // Right\n new StretchedSprite(new PIXI.Texture(texture.baseTexture)),\n // Top-Right\n new PIXI.Sprite(new PIXI.Texture(texture.baseTexture)),\n // Top\n new StretchedSprite(new PIXI.Texture(texture.baseTexture)),\n // Top-Left\n new PIXI.Sprite(new PIXI.Texture(texture.baseTexture)),\n // Left\n new StretchedSprite(new PIXI.Texture(texture.baseTexture)),\n // Bottom-Left\n new PIXI.Sprite(new PIXI.Texture(texture.baseTexture)),\n // Bottom\n new StretchedSprite(new PIXI.Texture(texture.baseTexture)),\n // Bottom-Right\n new PIXI.Sprite(new PIXI.Texture(texture.baseTexture)),\n ];\n\n this.setTexture(textureName, instanceContainer);\n this._spritesContainer.removeChildren();\n this._spritesContainer.addChild(this._centerSprite);\n for (let i = 0; i < this._borderSprites.length; ++i) {\n this._spritesContainer.addChild(this._borderSprites[i]);\n }\n this._wrapperContainer.addChild(this._spritesContainer);\n instanceContainer\n .getLayer('')\n .getRenderer()\n .addRendererObject(this._wrapperContainer, runtimeObject.getZOrder());\n }\n\n getRendererObject() {\n return this._wrapperContainer;\n }\n\n ensureUpToDate() {\n if (this._spritesContainer.visible && this._wasRendered) {\n // PIXI uses PIXI.SCALE_MODES.LINEAR for the cached image:\n // this._spritesContainer._cacheData.sprite._texture.baseTexture.scaleMode\n // There seems to be no way to configure this so the optimization is disabled.\n if (\n this._centerSprite.texture.baseTexture.scaleMode !==\n PIXI.SCALE_MODES.NEAREST\n ) {\n // Cache the rendered sprites as a bitmap to speed up rendering when\n // lots of panel sprites are on the scene.\n // Sadly, because of this, we need a wrapper container to workaround\n // a PixiJS issue with alpha (see updateOpacity).\n this._spritesContainer.cacheAsBitmap = true;\n }\n }\n this._wasRendered = true;\n }\n\n updateOpacity(): void {\n // The alpha is updated on a wrapper around the sprite because a known bug\n // in Pixi will create a flicker when cacheAsBitmap is set to true.\n // (see https://github.com/pixijs/pixijs/issues/4610)\n this._wrapperContainer.alpha = this._object.opacity / 255;\n // When the opacity is updated, the cache must be invalidated, otherwise\n // there is a risk of the panel sprite has been cached previously with a\n // different opacity (and cannot be updated anymore).\n this._spritesContainer.cacheAsBitmap = false;\n }\n\n updateAngle(): void {\n this._wrapperContainer.rotation = gdjs.toRad(this._object.angle);\n }\n\n updatePosition(): void {\n this._wrapperContainer.position.x =\n this._object.x + this._object._width / 2;\n this._wrapperContainer.position.y =\n this._object.y + this._object._height / 2;\n }\n\n _updateLocalPositions() {\n const obj = this._object;\n this._centerSprite.position.x = obj._lBorder;\n this._centerSprite.position.y = obj._tBorder;\n\n //Right\n this._borderSprites[0].position.x = obj._width - obj._rBorder;\n this._borderSprites[0].position.y = obj._tBorder;\n\n //Top-right\n this._borderSprites[1].position.x =\n obj._width - this._borderSprites[1].width;\n this._borderSprites[1].position.y = 0;\n\n //Top\n this._borderSprites[2].position.x = obj._lBorder;\n this._borderSprites[2].position.y = 0;\n\n //Top-Left\n this._borderSprites[3].position.x = 0;\n this._borderSprites[3].position.y = 0;\n\n //Left\n this._borderSprites[4].position.x = 0;\n this._borderSprites[4].position.y = obj._tBorder;\n\n //Bottom-Left\n this._borderSprites[5].position.x = 0;\n this._borderSprites[5].position.y =\n obj._height - this._borderSprites[5].height;\n\n //Bottom\n this._borderSprites[6].position.x = obj._lBorder;\n this._borderSprites[6].position.y = obj._height - obj._bBorder;\n\n //Bottom-Right\n this._borderSprites[7].position.x =\n obj._width - this._borderSprites[7].width;\n this._borderSprites[7].position.y =\n obj._height - this._borderSprites[7].height;\n }\n\n _updateSpritesAndTexturesSize() {\n const obj = this._object;\n this._centerSprite.width = Math.max(\n obj._width - obj._rBorder - obj._lBorder,\n 0\n );\n this._centerSprite.height = Math.max(\n obj._height - obj._tBorder - obj._bBorder,\n 0\n );\n\n //Right\n this._borderSprites[0].width = obj._rBorder;\n this._borderSprites[0].height = Math.max(\n obj._height - obj._tBorder - obj._bBorder,\n 0\n );\n\n //Top\n this._borderSprites[2].height = obj._tBorder;\n this._borderSprites[2].width = Math.max(\n obj._width - obj._rBorder - obj._lBorder,\n 0\n );\n\n //Left\n this._borderSprites[4].width = obj._lBorder;\n this._borderSprites[4].height = Math.max(\n obj._height - obj._tBorder - obj._bBorder,\n 0\n );\n\n //Bottom\n this._borderSprites[6].height = obj._bBorder;\n this._borderSprites[6].width = Math.max(\n obj._width - obj._rBorder - obj._lBorder,\n 0\n );\n this._wasRendered = true;\n this._spritesContainer.cacheAsBitmap = false;\n }\n\n setTexture(\n textureName: string,\n instanceContainer: gdjs.RuntimeInstanceContainer\n ): void {\n const obj = this._object;\n const texture = instanceContainer\n .getGame()\n .getImageManager()\n .getPIXITexture(textureName).baseTexture;\n this._textureWidth = texture.width;\n this._textureHeight = texture.height;\n\n function makeInsideTexture(rect) {\n if (rect.width < 0) {\n rect.width = 0;\n }\n if (rect.height < 0) {\n rect.height = 0;\n }\n if (rect.x < 0) {\n rect.x = 0;\n }\n if (rect.y < 0) {\n rect.y = 0;\n }\n if (rect.x > texture.width) {\n rect.x = texture.width;\n }\n if (rect.y > texture.height) {\n rect.y = texture.height;\n }\n if (rect.x + rect.width > texture.width) {\n rect.width = texture.width - rect.x;\n }\n if (rect.y + rect.height > texture.height) {\n rect.height = texture.height - rect.y;\n }\n return rect;\n }\n this._centerSprite.texture.destroy(false);\n this._centerSprite.texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n obj._lBorder,\n obj._tBorder,\n texture.width - obj._lBorder - obj._rBorder,\n texture.height - obj._tBorder - obj._bBorder\n )\n )\n );\n\n //Top, Bottom, Right, Left borders:\n this._borderSprites[0].texture.destroy(false);\n this._borderSprites[0].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n texture.width - obj._rBorder,\n obj._tBorder,\n obj._rBorder,\n texture.height - obj._tBorder - obj._bBorder\n )\n )\n );\n this._borderSprites[2].texture.destroy(false);\n this._borderSprites[2].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n obj._lBorder,\n 0,\n texture.width - obj._lBorder - obj._rBorder,\n obj._tBorder\n )\n )\n );\n this._borderSprites[4].texture.destroy(false);\n this._borderSprites[4].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n 0,\n obj._tBorder,\n obj._lBorder,\n texture.height - obj._tBorder - obj._bBorder\n )\n )\n );\n this._borderSprites[6].texture.destroy(false);\n this._borderSprites[6].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n obj._lBorder,\n texture.height - obj._bBorder,\n texture.width - obj._lBorder - obj._rBorder,\n obj._bBorder\n )\n )\n );\n this._borderSprites[1].texture.destroy(false);\n this._borderSprites[1].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n texture.width - obj._rBorder,\n 0,\n obj._rBorder,\n obj._tBorder\n )\n )\n );\n this._borderSprites[3].texture.destroy(false);\n this._borderSprites[3].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(new PIXI.Rectangle(0, 0, obj._lBorder, obj._tBorder))\n );\n this._borderSprites[5].texture.destroy(false);\n this._borderSprites[5].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n 0,\n texture.height - obj._bBorder,\n obj._lBorder,\n obj._bBorder\n )\n )\n );\n this._borderSprites[7].texture.destroy(false);\n this._borderSprites[7].texture = new PIXI.Texture(\n texture,\n makeInsideTexture(\n new PIXI.Rectangle(\n texture.width - obj._rBorder,\n texture.height - obj._bBorder,\n obj._rBorder,\n obj._bBorder\n )\n )\n );\n this._updateSpritesAndTexturesSize();\n this._updateLocalPositions();\n this.updatePosition();\n this._wrapperContainer.pivot.x = this._object._width / 2;\n this._wrapperContainer.pivot.y = this._object._height / 2;\n }\n\n updateWidth(): void {\n this._wrapperContainer.pivot.x = this._object._width / 2;\n this._updateSpritesAndTexturesSize();\n this._updateLocalPositions();\n this.updatePosition();\n }\n\n updateHeight(): void {\n this._wrapperContainer.pivot.y = this._object._height / 2;\n this._updateSpritesAndTexturesSize();\n this._updateLocalPositions();\n this.updatePosition();\n }\n\n setColor(rgbColor): void {\n const colors = rgbColor.split(';');\n if (colors.length < 3) {\n return;\n }\n this._centerSprite.tint = gdjs.rgbToHexNumber(\n parseInt(colors[0], 10),\n parseInt(colors[1], 10),\n parseInt(colors[2], 10)\n );\n for (\n let borderCounter = 0;\n borderCounter < this._borderSprites.length;\n borderCounter++\n ) {\n this._borderSprites[borderCounter].tint = gdjs.rgbToHexNumber(\n parseInt(colors[0], 10),\n parseInt(colors[1], 10),\n parseInt(colors[2], 10)\n );\n }\n this._spritesContainer.cacheAsBitmap = false;\n }\n\n getColor() {\n const rgb = new PIXI.Color(this._centerSprite.tint).toRgbArray();\n return (\n Math.floor(rgb[0] * 255) +\n ';' +\n Math.floor(rgb[1] * 255) +\n ';' +\n Math.floor(rgb[2] * 255)\n );\n }\n\n getTextureWidth() {\n return this._textureWidth;\n }\n\n getTextureHeight() {\n return this._textureHeight;\n }\n\n destroy() {\n // Destroy textures because they are instantiated by this class:\n // all textures of borderSprites and centerSprite are \"owned\" by them.\n for (const borderSprite of this._borderSprites) {\n borderSprite.destroy({ texture: true });\n }\n this._centerSprite.destroy({ texture: true });\n // Destroy the containers without handling children because they are\n // already handled above.\n this._wrapperContainer.destroy(false);\n this._spritesContainer.destroy(false);\n }\n }\n\n export const PanelSpriteRuntimeObjectRenderer = PanelSpriteRuntimeObjectPixiRenderer;\n export type PanelSpriteRuntimeObjectRenderer = PanelSpriteRuntimeObjectPixiRenderer;\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|