gdcore-tools 2.0.0-gd-v5.5.224-autobuild → 2.0.0-gd-v5.5.226-autobuild
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/Runtime/CustomRuntimeObject.js +1 -1
- package/dist/Runtime/CustomRuntimeObject.js.map +2 -2
- package/dist/Runtime/CustomRuntimeObject2D.js.map +2 -2
- package/dist/Runtime/Extensions/3D/A_RuntimeObject3D.js.map +2 -2
- package/dist/Runtime/Extensions/3D/Base3DBehavior.js.map +2 -2
- package/dist/Runtime/Extensions/3D/Cube3DRuntimeObject.js +1 -1
- package/dist/Runtime/Extensions/3D/Cube3DRuntimeObject.js.map +2 -2
- package/dist/Runtime/Extensions/3D/Cube3DRuntimeObjectPixiRenderer.js +1 -1
- package/dist/Runtime/Extensions/3D/Cube3DRuntimeObjectPixiRenderer.js.map +2 -2
- package/dist/Runtime/Extensions/3D/CustomRuntimeObject3D.js.map +2 -2
- package/dist/Runtime/Extensions/3D/CustomRuntimeObject3DRenderer.js +1 -1
- package/dist/Runtime/Extensions/3D/CustomRuntimeObject3DRenderer.js.map +2 -2
- package/dist/Runtime/Extensions/3D/JsExtension.js +219 -108
- package/dist/Runtime/Extensions/3D/Model3DRuntimeObject.js +1 -1
- package/dist/Runtime/Extensions/3D/Model3DRuntimeObject.js.map +2 -2
- package/dist/Runtime/Extensions/3D/Model3DRuntimeObject3DRenderer.js +1 -1
- package/dist/Runtime/Extensions/3D/Model3DRuntimeObject3DRenderer.js.map +2 -2
- package/dist/Runtime/Extensions/AdMob/JsExtension.js +63 -1
- package/dist/Runtime/Extensions/AdMob/admobtools.js +1 -1
- package/dist/Runtime/Extensions/AdMob/admobtools.js.map +2 -2
- package/dist/Runtime/Extensions/AdvancedWindow/electron-advancedwindowtools.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/BBText/JsExtension.js +10 -9
- package/dist/Runtime/Extensions/BBText/bbtextruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/BitmapText/JsExtension.js +4 -6
- package/dist/Runtime/Extensions/BitmapText/bitmaptextruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/BitmapText/bitmaptextruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/DialogueTree/dialoguetools.js.map +2 -2
- package/dist/Runtime/Extensions/DraggableBehavior/draggableruntimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/adjustment-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/advanced-bloom-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/ascii-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/bevel-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/black-and-white-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/blending-mode-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/brightness-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/bulge-pinch-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/color-map-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/color-replace-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/crt-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/displacement-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/dot-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/drop-shadow-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/glitch-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/glow-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/godray-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/kawase-blur-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/noise-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/old-film-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/outline-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/pixelate-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/pixi-filters/types/drop-shadow/types.d.ts +10 -4
- package/dist/Runtime/Extensions/Effects/radial-blur-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/reflection-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/rgb-split-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/sepia-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/shockwave-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/tilt-shift-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/twist-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/Effects/zoom-blur-pixi-filter.js.map +2 -2
- package/dist/Runtime/Extensions/ExampleJsExtension/dummyeffect.js.map +2 -2
- package/dist/Runtime/Extensions/FacebookInstantGames/facebookinstantgamestools.js.map +2 -2
- package/dist/Runtime/Extensions/Firebase/A_firebasejs/firebase.d.ts +5 -4
- package/dist/Runtime/Extensions/Firebase/B_firebasetools/D_cloudfirestoretools.js.map +2 -2
- package/dist/Runtime/Extensions/Firebase/B_firebasetools/D_remoteconfigtools.js.map +2 -2
- package/dist/Runtime/Extensions/Firebase/JsExtension.js +21 -21
- package/dist/Runtime/Extensions/JsExtensionTypes.d.ts +1 -0
- package/dist/Runtime/Extensions/Leaderboards/leaderboardstools.js.map +2 -2
- package/dist/Runtime/Extensions/Lighting/JsExtension.js +2 -2
- package/dist/Runtime/Extensions/Lighting/lightobstacleruntimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/Lighting/lightruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/Lighting/lightruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/LinkedObjects/linkedobjects.js.map +2 -2
- package/dist/Runtime/Extensions/Multiplayer/JsExtension.js +122 -0
- package/dist/Runtime/Extensions/Multiplayer/messageManager.js.map +2 -2
- package/dist/Runtime/Extensions/Multiplayer/multiplayerVariablesManager.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.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/Multiplayer/peerJsHelper.js +1 -1
- package/dist/Runtime/Extensions/Multiplayer/peerJsHelper.js.map +2 -2
- package/dist/Runtime/Extensions/Multiplayer/peerjs.d.ts +8 -10
- package/dist/Runtime/Extensions/P2P/peerjs.d.ts +8 -10
- package/dist/Runtime/Extensions/PanelSpriteObject/panelspriteruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/PanelSpriteObject/panelspriteruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/ParticleSystem/particleemitterobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/ParticleSystem/particleemitterobject.js.map +2 -2
- package/dist/Runtime/Extensions/ParticleSystem/pixi-particles-pixi-renderer.d.ts +2 -1
- package/dist/Runtime/Extensions/PathfindingBehavior/pathfindingobstacleruntimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/PathfindingBehavior/pathfindingruntimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/Physics2Behavior/JsExtension.js +106 -106
- package/dist/Runtime/Extensions/Physics2Behavior/box2d.d.ts +13 -7
- package/dist/Runtime/Extensions/Physics2Behavior/physics2runtimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/Physics3DBehavior/Physics3DRuntimeBehavior.js.map +2 -2
- package/dist/Runtime/Extensions/Physics3DBehavior/PhysicsCharacter3DRuntimeBehavior.js +1 -1
- package/dist/Runtime/Extensions/Physics3DBehavior/PhysicsCharacter3DRuntimeBehavior.js.map +2 -2
- package/dist/Runtime/Extensions/PhysicsBehavior/physicsruntimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/PlatformBehavior/platformerobjectruntimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/PlatformBehavior/platformruntimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/PlayerAuthentication/playerauthenticationcomponents.js.map +1 -1
- package/dist/Runtime/Extensions/PlayerAuthentication/playerauthenticationtools.js +1 -1
- package/dist/Runtime/Extensions/PlayerAuthentication/playerauthenticationtools.js.map +2 -2
- package/dist/Runtime/Extensions/PrimitiveDrawing/shapepainterruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/PrimitiveDrawing/shapepainterruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/SpatialSound/spatialsoundtools.js +1 -1
- package/dist/Runtime/Extensions/SpatialSound/spatialsoundtools.js.map +2 -2
- package/dist/Runtime/Extensions/Spine/JsExtension.js +5 -4
- package/dist/Runtime/Extensions/Spine/managers/pixi-spine-atlas-manager.js +1 -1
- package/dist/Runtime/Extensions/Spine/managers/pixi-spine-atlas-manager.js.map +2 -2
- package/dist/Runtime/Extensions/Spine/managers/pixi-spine-manager.js +1 -1
- package/dist/Runtime/Extensions/Spine/managers/pixi-spine-manager.js.map +2 -2
- package/dist/Runtime/Extensions/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.map +2 -2
- package/dist/Runtime/Extensions/Steamworks/JsExtension.js +12 -12
- package/dist/Runtime/Extensions/Steamworks/Z_steamworksinputtools.js.map +2 -2
- package/dist/Runtime/Extensions/Steamworks/steamworkstools.js.map +2 -2
- package/dist/Runtime/Extensions/TextEntryObject/textentryruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/TextInput/textinputruntimeobject-pixi-renderer.js +1 -1
- package/dist/Runtime/Extensions/TextInput/textinputruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/TextInput/textinputruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/TextObject/textruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/TextObject/textruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/TileMap/JsExtension.js +20 -18
- package/dist/Runtime/Extensions/TileMap/TileMapRuntimeManager.js.map +2 -2
- package/dist/Runtime/Extensions/TileMap/collision/TransformedTileMap.js.map +2 -2
- package/dist/Runtime/Extensions/TileMap/helper/dts/model/TileMapModel.d.ts +1 -3
- package/dist/Runtime/Extensions/TileMap/simpletilemapruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/TileMap/tilemapcollisionmaskruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/TileMap/tilemapruntimeobject-pixi-renderer.js.map +1 -1
- package/dist/Runtime/Extensions/TileMap/tilemapruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/TiledSpriteObject/tiledspriteruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/TiledSpriteObject/tiledspriteruntimeobject.js.map +2 -2
- package/dist/Runtime/Extensions/TopDownMovementBehavior/topdownmovementruntimebehavior.js.map +2 -2
- package/dist/Runtime/Extensions/TweenBehavior/tweenruntimebehavior.js.map +1 -1
- package/dist/Runtime/Extensions/Video/JsExtension.js +2 -1
- package/dist/Runtime/Extensions/Video/videoruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/Extensions/Video/videoruntimeobject.js.map +2 -2
- package/dist/Runtime/InAppTutorialMessage.js +6 -0
- package/dist/Runtime/InAppTutorialMessage.js.map +7 -0
- package/dist/Runtime/Model3DManager.js.map +2 -2
- package/dist/Runtime/ResourceLoader.js.map +2 -2
- package/dist/Runtime/RuntimeCustomObjectLayer.js +1 -1
- package/dist/Runtime/RuntimeCustomObjectLayer.js.map +2 -2
- package/dist/Runtime/RuntimeInstanceContainer.js.map +1 -1
- package/dist/Runtime/RuntimeLayer.js.map +2 -2
- package/dist/Runtime/SpriteAnimator.js.map +2 -2
- package/dist/Runtime/affinetransformation.js.map +1 -1
- package/dist/Runtime/debugger-client/abstract-debugger-client.js.map +2 -2
- package/dist/Runtime/debugger-client/hot-reloader.js +2 -2
- package/dist/Runtime/debugger-client/hot-reloader.js.map +2 -2
- package/dist/Runtime/debugger-client/websocket-debugger-client.js +1 -1
- package/dist/Runtime/debugger-client/websocket-debugger-client.js.map +2 -2
- package/dist/Runtime/events-tools/networktools.js +1 -1
- package/dist/Runtime/events-tools/networktools.js.map +2 -2
- package/dist/Runtime/fontfaceobserver-font-manager/fontfaceobserver-font-manager.js.map +2 -2
- package/dist/Runtime/gd.js.map +2 -2
- package/dist/Runtime/howler-sound-manager/howler-sound-manager.js +1 -1
- package/dist/Runtime/howler-sound-manager/howler-sound-manager.js.map +2 -2
- package/dist/Runtime/inputmanager.js.map +2 -2
- package/dist/Runtime/jsonmanager.js.map +2 -2
- package/dist/Runtime/layer.js.map +2 -2
- package/dist/Runtime/libs/nanomarkdown.js +5 -0
- package/dist/Runtime/libs/nanomarkdown.js.map +7 -0
- package/dist/Runtime/object-capabilities/AnimatableBehavior.js.map +2 -2
- package/dist/Runtime/object-capabilities/EffectBehavior.js.map +2 -2
- package/dist/Runtime/object-capabilities/FlippableBehavior.js.map +2 -2
- package/dist/Runtime/object-capabilities/OpacityBehavior.js.map +2 -2
- package/dist/Runtime/object-capabilities/ResizableBehavior.js.map +2 -2
- package/dist/Runtime/object-capabilities/ScalableBehavior.js.map +2 -2
- package/dist/Runtime/object-capabilities/TextContainerBehavior.js.map +2 -2
- package/dist/Runtime/pixi-renderers/CustomRuntimeObject2DPixiRenderer.js.map +2 -2
- package/dist/Runtime/pixi-renderers/DebuggerPixiRenderer.js.map +2 -2
- package/dist/Runtime/pixi-renderers/layer-pixi-renderer.js.map +2 -2
- package/dist/Runtime/pixi-renderers/loadingscreen-pixi-renderer.js.map +2 -2
- package/dist/Runtime/pixi-renderers/pixi-bitmapfont-manager.js.map +2 -2
- package/dist/Runtime/pixi-renderers/pixi-filters-tools.js.map +2 -2
- package/dist/Runtime/pixi-renderers/pixi-image-manager.js +1 -1
- package/dist/Runtime/pixi-renderers/pixi-image-manager.js.map +2 -2
- package/dist/Runtime/pixi-renderers/pixi.js +123 -177
- package/dist/Runtime/pixi-renderers/runtimegame-pixi-renderer.js +1 -1
- package/dist/Runtime/pixi-renderers/runtimegame-pixi-renderer.js.map +2 -2
- package/dist/Runtime/pixi-renderers/runtimescene-pixi-renderer.js.map +2 -2
- package/dist/Runtime/pixi-renderers/spriteruntimeobject-pixi-renderer.js.map +2 -2
- package/dist/Runtime/profiler.js.map +2 -2
- package/dist/Runtime/runtimegame.js +1 -1
- package/dist/Runtime/runtimegame.js.map +2 -2
- package/dist/Runtime/runtimeobject.js +1 -1
- package/dist/Runtime/runtimeobject.js.map +2 -2
- package/dist/Runtime/runtimescene.js.map +2 -2
- package/dist/Runtime/runtimewatermark.js.map +2 -2
- package/dist/Runtime/scenestack.js.map +2 -2
- package/dist/Runtime/spriteruntimeobject.js.map +2 -2
- package/dist/Runtime/variable.js.map +2 -2
- 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 +5 -2
- package/package.json +1 -1
|
@@ -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 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 // TODO: this should be sent by the backend, in case the lobby starts without a player 1.\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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",
|
|
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 type Lobby = {\n id: string;\n status: 'waiting' | 'starting' | 'playing' | 'migrating' | 'migrated';\n };\n\n type QuickJoinLobbyResponse =\n | { status: 'join-game'; lobby: Lobby }\n | { status: 'join-lobby'; lobby: Lobby }\n | { status: 'not-enough-players' }\n | { status: 'full' };\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 _quickJoinLobbyJustFailed = false;\n let _quickJoinLobbyFailureReason:\n | 'FULL'\n | 'NOT_ENOUGH_PLAYERS'\n | 'UNKNOWN'\n | null = null;\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 let _actionAfterJoiningLobby:\n | 'OPEN_LOBBY_PAGE'\n | 'JOIN_GAME'\n | 'START_GAME'\n | null = null;\n let _isQuickJoiningOrStartingAGame = false;\n let _lastQuickJoinRequestDoneAt: 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 _quickJoinLobbyJustFailed = 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 const platformInfo = runtimeGame.getPlatformInfo();\n url.searchParams.set(\n 'scm',\n platformInfo.supportedCompressionMethods.join(',')\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 =\n 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 =\n 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 const retryData = { times: 2, delayInMs: 500 };\n try {\n gdjs.evtTools.network.retryIfFailed(retryData, async () => {\n handlePeerIdEvent({ peerId, compressionMethod });\n });\n } catch (error) {\n logger.error(\n `Handling peerId message from websocket failed (after {${retryData.times}} times with a delay of ${retryData.delayInMs}ms). Not trying anymore.`\n );\n }\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 =\n gdjs.multiplayerComponents.getLobbiesIframe(runtimeScene);\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 onPeerUnavailable = (runtimeScene: gdjs.RuntimeScene) => {\n gdjs.multiplayerComponents.displayConnectionErrorNotification(\n runtimeScene\n );\n handleLeaveLobbyEvent();\n _actionAfterJoiningLobby = null;\n _quickJoinLobbyFailureReason = null;\n if (_isQuickJoiningOrStartingAGame)\n onLobbyQuickJoinFinished(runtimeScene);\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 { onPeerUnavailable: () => onPeerUnavailable(runtimeScene) }\n );\n } else {\n gdjs.multiplayerPeerJsHelper.useDefaultBrokerServer({\n onPeerUnavailable: () => onPeerUnavailable(runtimeScene),\n });\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 if (_actionAfterJoiningLobby === 'OPEN_LOBBY_PAGE') {\n openLobbiesWindow(runtimeScene);\n onLobbyQuickJoinFinished(runtimeScene);\n return;\n } else if (_actionAfterJoiningLobby === 'JOIN_GAME') {\n handleJoinGameMessage();\n return;\n } else if (_actionAfterJoiningLobby === 'START_GAME') {\n const retryData = { times: 2, delayInMs: 500 };\n try {\n gdjs.evtTools.network.retryIfFailed(retryData, async () => {\n sendPeerId();\n handleStartGameMessage();\n });\n } catch (error) {\n logger.error(\n `Sending of peerId message from websocket failed (after {${retryData.times}} times with a delay of ${retryData.delayInMs}ms). Not trying anymore.`\n );\n }\n return;\n }\n\n // Then we inform the lobbies window that the player has joined.\n const lobbiesIframe =\n gdjs.multiplayerComponents.getLobbiesIframe(runtimeScene);\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 =\n gdjs.multiplayerComponents.getLobbiesIframe(runtimeScene);\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 // TODO: this should be sent by the backend, in case the lobby starts without a player 1.\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 =\n gdjs.multiplayerComponents.getLobbiesIframe(runtimeScene);\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 // TODO: if 404, there's chance that it means the lobby is now closed. Display a message\n // to the player?\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 if (_isQuickJoiningOrStartingAGame)\n onLobbyQuickJoinFinished(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 throw new Error('Missing player peerId.');\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 join game message. Are you connected to a lobby?'\n );\n return;\n }\n // TODO: When the message is sent, it is expected to then receive a \"peerId\" message\n // from the websocket. This \"peerId\" message might not be sent for different reasons.\n // Should there be a security that checks if the \"peerId\" message has been received\n // in the next 10s or something more global that checks the lobby status after the player\n // has committed to open a connection with it?\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 =\n gdjs.multiplayerMessageManager\n .getConnectedPlayers()\n .map((player) => player.playerNumber);\n const playerNumbersWhoLeftDuringMigration =\n 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 =\n 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 throw new Error('Missing player peerId.');\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 _actionAfterJoiningLobby = null;\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 =\n gdjs.multiplayerComponents.getLobbiesIframe(runtimeScene);\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 const onLobbyQuickJoinFinished = (runtimeScene: gdjs.RuntimeScene) => {\n _isQuickJoiningOrStartingAGame = false;\n _actionAfterJoiningLobby = null;\n gdjs.multiplayerComponents.displayLoader(runtimeScene, false);\n };\n\n const quickJoinLobby = async (\n runtimeScene: gdjs.RuntimeScene,\n displayLoader: boolean,\n openLobbiesPageIfFailure: boolean\n ) => {\n if (_isQuickJoiningOrStartingAGame) return;\n const _gameId = gdjs.projectData.properties.projectUuid;\n if (!_gameId) {\n handleLobbiesError(\n runtimeScene,\n 'The game ID is missing, the quick join lobby action cannot continue.'\n );\n return;\n }\n\n _quickJoinLobbyFailureReason = null;\n _isQuickJoiningOrStartingAGame = true;\n if (displayLoader) {\n gdjs.multiplayerComponents.displayLoader(runtimeScene, true);\n }\n\n const quickJoinLobbyRelativeUrl = `/play/game/${_gameId}/public-lobby/action/quick-join`;\n const platformInfo = runtimeScene.getGame().getPlatformInfo();\n\n try {\n const quickJoinLobbyResponse: QuickJoinLobbyResponse =\n await gdjs.evtTools.network.retryIfFailed({ times: 2 }, () =>\n fetchAsPlayer({\n relativeUrl: quickJoinLobbyRelativeUrl,\n method: 'POST',\n dev: isUsingGDevelopDevelopmentEnvironment,\n body: JSON.stringify({\n isPreview: runtimeScene.getGame().isPreview(),\n gameVersion: runtimeScene.getGame().getGameData().properties\n .version,\n supportedCompressionMethods:\n platformInfo.supportedCompressionMethods,\n }),\n })\n );\n\n if (\n quickJoinLobbyResponse.status === 'full' ||\n quickJoinLobbyResponse.status === 'not-enough-players'\n ) {\n _quickJoinLobbyJustFailed = true;\n _quickJoinLobbyFailureReason =\n quickJoinLobbyResponse.status === 'full'\n ? 'FULL'\n : 'NOT_ENOUGH_PLAYERS';\n onLobbyQuickJoinFinished(runtimeScene);\n if (openLobbiesPageIfFailure) {\n openLobbiesWindow(runtimeScene);\n }\n return;\n }\n\n if (quickJoinLobbyResponse.status === 'join-game') {\n if (quickJoinLobbyResponse.lobby.status === 'waiting') {\n _actionAfterJoiningLobby = 'START_GAME';\n } else if (quickJoinLobbyResponse.lobby.status === 'playing') {\n _actionAfterJoiningLobby = 'JOIN_GAME';\n } else {\n throw new Error(\n `Lobby in wrong status: ${quickJoinLobbyResponse.status}`\n );\n }\n } else {\n if (_connectionId) {\n // Already connected to a lobby.\n onLobbyQuickJoinFinished(runtimeScene);\n openLobbiesWindow(runtimeScene);\n return;\n } else {\n _actionAfterJoiningLobby = 'OPEN_LOBBY_PAGE';\n }\n }\n handleJoinLobbyEvent(runtimeScene, quickJoinLobbyResponse.lobby.id);\n } catch (error) {\n logger.error('An error occurred while joining a lobby:', error);\n _quickJoinLobbyJustFailed = true;\n _quickJoinLobbyFailureReason = 'UNKNOWN';\n onLobbyQuickJoinFinished(runtimeScene);\n if (openLobbiesPageIfFailure) {\n openLobbiesWindow(runtimeScene);\n }\n }\n };\n\n export const authenticateAndQuickJoinLobby = async (\n runtimeScene: gdjs.RuntimeScene,\n displayLoader: boolean,\n openLobbiesPageIfFailure: boolean\n ) => {\n const requestDoneAt = Date.now();\n if (_lastQuickJoinRequestDoneAt) {\n if (requestDoneAt - _lastQuickJoinRequestDoneAt < 500) {\n _lastQuickJoinRequestDoneAt = requestDoneAt;\n logger.warn(\n 'Last request to quick join a lobby was sent too little time ago. Ignoring this one.'\n );\n return;\n }\n } else {\n _lastQuickJoinRequestDoneAt = requestDoneAt;\n }\n\n const playerId = gdjs.playerAuthentication.getUserId();\n const playerToken = gdjs.playerAuthentication.getUserToken();\n if (!playerId || !playerToken) {\n _isWaitingForLogin = true;\n const { status } =\n await gdjs.playerAuthentication.openAuthenticationWindow(runtimeScene)\n .promise;\n _isWaitingForLogin = false;\n\n if (status === 'logged') {\n await quickJoinLobby(\n runtimeScene,\n displayLoader,\n openLobbiesPageIfFailure\n );\n }\n\n return;\n }\n await quickJoinLobby(\n runtimeScene,\n displayLoader,\n openLobbiesPageIfFailure\n );\n };\n\n export const isSearchingForLobbyToJoin = (\n runtimeScene: gdjs.RuntimeScene\n ) => {\n return _isQuickJoiningOrStartingAGame;\n };\n\n export const hasQuickJoinJustFailed = (runtimeScene: gdjs.RuntimeScene) => {\n return _quickJoinLobbyJustFailed;\n };\n\n export const getQuickJoinFailureReason = () => {\n return _quickJoinLobbyFailureReason;\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 { status } =\n await gdjs.playerAuthentication.openAuthenticationWindow(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 =\n gdjs.multiplayerComponents.getLobbiesRootContainer(runtimeScene);\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(
|
|
1
|
+
var gdjs;(function(m){const l=new m.Logger("Multiplayer");let j;(function(n){const I=e=>typeof e=="object"&&e!==null&&typeof e.messageName=="string"&&typeof e.data=="object";class A{constructor(t,s){this.data=t,this.sender=s}getData(){return this.data}getSender(){return this.sender}}n.MessageData=A;class L{constructor(t){this.data=[];this.messageName=t}getName(){return this.messageName}getMessages(){return this.data}pushMessage(t,s){this.data.push(new A(t,s))}}n.MessagesList=L;let o={debug:1},r=null;const p=new Map,h=new Map;let y=!1,S=null;const M=[],D=[];let u="none";n.setCompressionMethod=e=>{u=e};async function O(e){if(u==="none")return JSON.stringify(e);const t=u==="cs:gzip"?"gzip":"deflate",s=JSON.stringify(e),i=new TextEncoder().encode(s),g=new CompressionStream(t),f=g.writable.getWriter();f.write(i),f.close();const N=g.readable.getReader(),b=[];for(;;){const{done:d,value:x}=await N.read();if(d)break;b.push(x)}return new Uint8Array(b.reduce((d,x)=>d.concat(Array.from(x)),[]))}async function U(e){if(u==="none"){if(typeof e!="string"){l.error(`Error while parsing message using compressionMethod ${u}: received data is not a string.`);return}try{return JSON.parse(e)}catch(c){l.error(`Error while parsing message: ${c.toString()}`);return}}const t=u==="cs:gzip"?"gzip":"deflate",s=new DecompressionStream(t),a=s.writable.getWriter();a.write(e),a.close();const g=s.readable.getReader(),f=[];for(;;){const{done:c,value:d}=await g.read();if(c)break;f.push(d)}const k=new Uint8Array(f.reduce((c,d)=>c.concat(Array.from(d)),[])),b=new TextDecoder().decode(k);try{return JSON.parse(b)}catch(c){l.error(`Error while parsing message: ${c.toString()}`);return}}n.getOrCreateMessagesList=e=>{const t=h.get(e);if(t)return t;const s=new L(e);return h.set(e,s),s};const R=()=>{y=!0,S&&(n.connect(S),S=null)},v=e=>{p.set(e.peer,e),e.on("data",async t=>{if(I(t)){const s=n.getOrCreateMessagesList(t.messageName),a=e.peer,i=await U(t.data);if(!i)return;s.pushMessage(i,a)}}),e.on("error",()=>{w(e.peer)}),e.on("close",()=>{w(e.peer)}),e.on("iceStateChanged",t=>{t==="disconnected"&&w(e.peer)}),function t(){e.peerConnection&&(e.peerConnection.connectionState==="failed"||e.peerConnection.connectionState==="disconnected"||e.peerConnection.connectionState==="closed")?w(e.peer):setTimeout(t,1e3)}()},w=e=>{!p.has(e)||(M.push(e),p.delete(e))},C=(e={})=>{r===null&&(r=new Peer(o),r.on("open",()=>{R()}),r.on("error",t=>{e.onPeerUnavailable&&t.type==="peer-unavailable"?(l.error("Peer is unavailable."),e.onPeerUnavailable()):l.error(`PeerJS error (${t.type||"unknown"}):`,t)}),r.on("connection",t=>{t.on("open",()=>{v(t),D.push(t.peer)})}),r.on("close",()=>{r=null,C(e)}),r.on("disconnected",r.reconnect))};n.useDefaultBrokerServer=C,n.connect=e=>{if(r===null||!y){S=e;return}const t=r.connect(e);t.on("open",()=>{v(t)})},n.disconnectFromAllPeers=()=>{for(const e of p.values())e.close()},n.sendDataTo=async(e,t,s)=>{if(!e.length)return;const a=await O(s);for(const i of e){const g=p.get(i);g&&g.send({messageName:t,data:a})}},n.getAllMessagesMap=()=>h,n.useCustomBrokerServer=(e,t,s,a,i,g={})=>{Object.assign(o,{host:e,port:t,path:s,secure:i,key:a.length===0?"peerjs":a}),C(g)},n.useCustomICECandidate=(e,t,s)=>{o.config=o.config||{},o.config.iceServers=o.config.iceServers||[],o.config.iceServers.push({urls:e,username:t,credential:s})},n.forceUseRelayServer=e=>{o.config=o.config||{},o.config.iceTransportPolicy=e?"relay":"all"},n.getCurrentId=()=>r===null?"":r.id,n.isReady=()=>y,n.getJustDisconnectedPeers=()=>M,n.getAllPeers=()=>Array.from(p.keys()),m.callbacksRuntimeScenePostEvents.push(()=>{for(const e of h.values())e.getMessages().length=0;M.length>0&&(M.length=0),D.length>0&&(D.length=0)})})(j=m.multiplayerPeerJsHelper||(m.multiplayerPeerJsHelper={}))})(gdjs||(gdjs={}));
|
|
2
2
|
//# sourceMappingURL=peerJsHelper.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../../../GDevelop/Extensions/Multiplayer/peerJsHelper.ts"],
|
|
4
|
-
"sourcesContent": ["/// <reference path=\"peerjs.d.ts\" />\nnamespace gdjs {\n const logger = new gdjs.Logger('Multiplayer');\n export namespace multiplayerPeerJsHelper {\n /**\n * The type of the data that is sent across peerjs.\n * We use UInt8Array to send compressed data, but we only manipulate objects once received.\n */\n type NetworkMessage = {\n messageName: string;\n data: Uint8Array | string;\n };\n\n export type CompressionMethod = 'none' | 'cs:gzip' | 'cs:deflate';\n\n /**\n * Helper to discard invalid messages when received.\n */\n const isValidNetworkMessage = (\n message: unknown\n ): message is NetworkMessage =>\n typeof message === 'object' &&\n message !== null &&\n typeof message['messageName'] === 'string' &&\n typeof message['data'] === 'object';\n\n export interface IMessageData {\n readonly data: any; // The data sent with the message, an object with unknown content.\n readonly sender: String;\n getData(): any;\n getSender(): string;\n }\n /**\n * The data bound to a message name.\n */\n export class MessageData implements IMessageData {\n public readonly data: any;\n public readonly sender: string;\n constructor(data: object, sender: string) {\n this.data = data;\n this.sender = sender;\n }\n public getData(): any {\n return this.data;\n }\n public getSender(): string {\n return this.sender;\n }\n }\n\n export interface IMessagesList {\n getName(): string;\n getMessages(): IMessageData[];\n pushMessage(data: object, sender: string): void;\n }\n export class MessagesList implements IMessagesList {\n private readonly data: IMessageData[] = [];\n private readonly messageName: string;\n\n constructor(messageName: string) {\n this.messageName = messageName;\n }\n\n public getName(): string {\n return this.messageName;\n }\n\n public getMessages(): IMessageData[] {\n return this.data;\n }\n\n public pushMessage(data: object, sender: string): void {\n this.data.push(new MessageData(data, sender));\n }\n }\n\n /**\n * The peer to peer configuration.\n */\n let peerConfig: Peer.PeerJSOption = { debug: 1 };\n\n /**\n * The p2p client.\n */\n let peer: Peer<NetworkMessage> | null = null;\n\n /**\n * All connected p2p clients, keyed by their ID.\n */\n const connections = new Map<string, Peer.DataConnection<NetworkMessage>>();\n\n /**\n * Contains a map of message triggered by other p2p clients.\n * It is keyed by the event name.\n */\n const allMessages = new Map<string, IMessagesList>();\n\n /**\n * True if PeerJS is initialized and ready.\n */\n let ready = false;\n\n /**\n * List of IDs of peers that just disconnected.\n */\n const justDisconnectedPeers: string[] = [];\n\n /**\n * List of IDs of peers that just remotely initiated a connection.\n */\n const justConnectedPeers: string[] = [];\n\n /**\n * The compression method used to compress data sent over the network.\n */\n let compressionMethod: CompressionMethod = 'none';\n export const setCompressionMethod = (method: CompressionMethod) => {\n compressionMethod = method;\n };\n\n /**\n * Helper function to compress data sent over the network.\n */\n async function compressData(data: object): Promise<Uint8Array | string> {\n if (compressionMethod === 'none') {\n // If no compression is used, we just stringify the data,\n // PeerJS will compress it to binary data.\n const jsonString = JSON.stringify(data);\n return jsonString;\n }\n\n const compressionStreamFormat =\n compressionMethod === 'cs:gzip' ? 'gzip' : 'deflate';\n\n const jsonString = JSON.stringify(data);\n const encoder = new TextEncoder();\n const array = encoder.encode(jsonString);\n\n // @ts-ignore - We checked that CompressionStream is available in the browser.\n const cs = new CompressionStream(compressionStreamFormat);\n const writer = cs.writable.getWriter();\n writer.write(array);\n writer.close();\n\n const compressedStream = cs.readable;\n const reader = compressedStream.getReader();\n const chunks: any[] = [];\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n const compressedData = new Uint8Array(\n chunks.reduce((acc, chunk) => acc.concat(Array.from(chunk)), [])\n );\n return compressedData;\n }\n\n /**\n * Helper function to decompress data received over the network.\n * It returns the parsed JSON object, if valid, or undefined.\n */\n async function decompressData(\n receivedData: Uint8Array | string\n ): Promise<object | undefined> {\n if (compressionMethod === 'none') {\n // If no compression is used, we just parse the data.\n if (typeof receivedData !== 'string') {\n logger.error(\n `Error while parsing message using compressionMethod ${compressionMethod}: received data is not a string.`\n );\n return;\n }\n\n try {\n const parsedData = JSON.parse(receivedData);\n return parsedData;\n } catch (e) {\n logger.error(`Error while parsing message: ${e.toString()}`);\n return;\n }\n }\n const compressionStreamFormat =\n compressionMethod === 'cs:gzip' ? 'gzip' : 'deflate';\n\n // @ts-ignore - We checked that DecompressionStream is available in the browser.\n const ds = new DecompressionStream(compressionStreamFormat);\n const writer = ds.writable.getWriter();\n writer.write(receivedData);\n writer.close();\n\n const decompressedStream = ds.readable;\n const reader = decompressedStream.getReader();\n const chunks: any[] = [];\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n const decompressedData = new Uint8Array(\n chunks.reduce((acc, chunk) => acc.concat(Array.from(chunk)), [])\n );\n const decoder = new TextDecoder();\n const jsonStringData = decoder.decode(decompressedData); // Convert Uint8Array back to string\n try {\n const parsedData = JSON.parse(jsonStringData);\n return parsedData;\n } catch (e) {\n logger.error(`Error while parsing message: ${e.toString()}`);\n return;\n }\n }\n\n /**\n * Helper function to get the messages list for a given message name.\n */\n export const getOrCreateMessagesList = (\n messageName: string\n ): IMessagesList => {\n const messagesList = allMessages.get(messageName);\n if (messagesList) return messagesList;\n const newMessagesList = new MessagesList(messageName);\n allMessages.set(messageName, newMessagesList);\n return newMessagesList;\n };\n\n /**\n * Internal function called when a connection with a remote peer is initiated.\n * @param connection The DataConnection of the peer\n */\n const _onConnect = (connection: Peer.DataConnection<NetworkMessage>) => {\n connections.set(connection.peer, connection);\n connection.on('data', async (data) => {\n if (isValidNetworkMessage(data)) {\n const messagesList = getOrCreateMessagesList(data.messageName);\n const messageSender = connection.peer;\n const decompressedData = await decompressData(data.data);\n if (!decompressedData) return;\n\n messagesList.pushMessage(decompressedData, messageSender);\n }\n });\n\n // Close event is only for graceful disconnection,\n // but we want onDisconnect to trigger for any type of disconnection,\n // so we register a listener for both event types.\n connection.on('error', () => {\n _onDisconnect(connection.peer);\n });\n connection.on('close', () => {\n _onDisconnect(connection.peer);\n });\n connection.on('iceStateChanged', (state) => {\n if (state === 'disconnected') {\n _onDisconnect(connection.peer);\n }\n });\n\n // Regularly check for disconnection as the built in way is not reliable.\n (function disconnectChecker() {\n if (\n connection.peerConnection &&\n (connection.peerConnection.connectionState === 'failed' ||\n connection.peerConnection.connectionState === 'disconnected' ||\n connection.peerConnection.connectionState === 'closed')\n ) {\n _onDisconnect(connection.peer);\n } else {\n setTimeout(disconnectChecker, 1000);\n }\n })();\n };\n\n /**\n * Internal function called when a remote client disconnects.\n * @param connectionID The ID of the peer that disconnected.\n */\n const _onDisconnect = (connectionID: string) => {\n if (!connections.has(connectionID)) return;\n justDisconnectedPeers.push(connectionID);\n connections.delete(connectionID);\n };\n\n /**\n * Internal function called to initialize PeerJS after it\n * has been configured.\n */\n const loadPeerJS = () => {\n if (peer !== null) return;\n peer = new Peer(peerConfig);\n peer.on('open', () => {\n ready = true;\n });\n peer.on('error', (errorMessage) => {\n logger.error('PeerJS error:', errorMessage);\n });\n peer.on('connection', (connection) => {\n connection.on('open', () => {\n _onConnect(connection);\n justConnectedPeers.push(connection.peer);\n });\n });\n peer.on('close', () => {\n peer = null;\n loadPeerJS();\n });\n peer.on('disconnected', peer.reconnect);\n };\n\n /**\n * Connects to another p2p client.\n * @param id - The other client's ID.\n */\n export const connect = (id: string) => {\n if (peer === null) return;\n const connection = peer.connect(id);\n connection.on('open', () => {\n _onConnect(connection);\n });\n };\n\n /**\n * Disconnects from all other p2p clients.\n */\n export const disconnectFromAllPeers = () => {\n for (const connection of connections.values()) connection.close();\n };\n\n /**\n * Send a message to a specific peer.\n * @param ids - The IDs of the clients to send the event to.\n * @param messageName - The event to trigger.\n * @param eventData - Additional data to send with the event.\n */\n export const sendDataTo = async (\n ids: string[],\n messageName: string,\n messageData: object\n ) => {\n if (!ids.length) return;\n\n const compressedData = await compressData(messageData);\n\n for (const id of ids) {\n const connection = connections.get(id);\n if (connection) {\n connection.send({\n messageName,\n data: compressedData,\n });\n }\n }\n };\n\n export const getAllMessagesMap = () => allMessages;\n\n /**\n * Connects to a custom broker server.\n * @param host The host of the broker server.\n * @param port The port of the broker server.\n * @param path The path (part of the url after the host) to the broker server.\n * @param key Optional password to connect to the broker server.\n * @param ssl Use ssl?\n */\n export const useCustomBrokerServer = (\n host: string,\n port: number,\n path: string,\n key: string,\n ssl: boolean\n ) => {\n Object.assign(peerConfig, {\n host,\n port,\n path,\n secure: ssl,\n // All servers have \"peerjs\" as default key\n key: key.length === 0 ? 'peerjs' : key,\n });\n loadPeerJS();\n };\n\n export const useDefaultBrokerServer = loadPeerJS;\n\n /**\n * Adds an ICE server candidate, and removes the default ones provided by PeerJs. Must be called before connecting to a broker.\n * @param urls The URL of the STUN/TURN server.\n * @param username An optional username to send to the server.\n * @param credential An optional password to send to the server.\n */\n export const useCustomICECandidate = (\n urls: string,\n username?: string,\n credential?: string\n ) => {\n peerConfig.config = peerConfig.config || {};\n peerConfig.config.iceServers = peerConfig.config.iceServers || [];\n peerConfig.config.iceServers.push({\n urls,\n username,\n credential,\n });\n };\n\n /**\n * Forces the usage of a relay (TURN) server, to avoid sharing IP addresses with the other peers.\n * @param shouldUseRelayServer Whether relay-only should be enabled or disabled.\n */\n export const forceUseRelayServer = (shouldUseRelayServer: boolean) => {\n peerConfig.config = peerConfig.config || {};\n peerConfig.config.iceTransportPolicy = shouldUseRelayServer\n ? 'relay'\n : 'all';\n };\n\n /**\n * Returns the own current peer ID.\n * @see Peer.id\n */\n export const getCurrentId = (): string => {\n if (peer == undefined) return '';\n return peer.id || '';\n };\n\n /**\n * Returns true once PeerJS finished initialization.\n * @see ready\n */\n export const isReady = () => ready;\n\n /**\n * Return peers that have disconnected in the frame.\n */\n export const getJustDisconnectedPeers = () => justDisconnectedPeers;\n\n /**\n * Returns the list of all currently connected peers.\n */\n export const getAllPeers = () => Array.from(connections.keys());\n\n gdjs.callbacksRuntimeScenePostEvents.push(() => {\n // Clear the list of messages at the end of the frame, assuming they've been all processed.\n for (const messagesList of allMessages.values()) {\n messagesList.getMessages().length = 0;\n }\n // Clear the list of just connected and disconnected peers.\n if (justDisconnectedPeers.length > 0) {\n justDisconnectedPeers.length = 0;\n }\n if (justConnectedPeers.length > 0) {\n justConnectedPeers.length = 0;\n }\n });\n }\n}\n"],
|
|
5
|
-
"mappings": "AACA,GAAU,MAAV,UAAU,EAAV,CACE,KAAM,GAAS,GAAI,GAAK,OAAO,eACxB,GAAU,GAAV,UAAU,EAAV,
|
|
4
|
+
"sourcesContent": ["/// <reference path=\"peerjs.d.ts\" />\nnamespace gdjs {\n const logger = new gdjs.Logger('Multiplayer');\n export namespace multiplayerPeerJsHelper {\n /**\n * The type of the data that is sent across peerjs.\n * We use UInt8Array to send compressed data, but we only manipulate objects once received.\n */\n type NetworkMessage = {\n messageName: string;\n data: Uint8Array | string;\n };\n\n type PeerJSInitOptions = {\n onPeerUnavailable?: () => void;\n };\n\n export type CompressionMethod = 'none' | 'cs:gzip' | 'cs:deflate';\n\n /**\n * Helper to discard invalid messages when received.\n */\n const isValidNetworkMessage = (\n message: unknown\n ): message is NetworkMessage =>\n typeof message === 'object' &&\n message !== null &&\n typeof message['messageName'] === 'string' &&\n typeof message['data'] === 'object';\n\n export interface IMessageData {\n readonly data: any; // The data sent with the message, an object with unknown content.\n readonly sender: String;\n getData(): any;\n getSender(): string;\n }\n /**\n * The data bound to a message name.\n */\n export class MessageData implements IMessageData {\n public readonly data: any;\n public readonly sender: string;\n constructor(data: object, sender: string) {\n this.data = data;\n this.sender = sender;\n }\n public getData(): any {\n return this.data;\n }\n public getSender(): string {\n return this.sender;\n }\n }\n\n export interface IMessagesList {\n getName(): string;\n getMessages(): IMessageData[];\n pushMessage(data: object, sender: string): void;\n }\n export class MessagesList implements IMessagesList {\n private readonly data: IMessageData[] = [];\n private readonly messageName: string;\n\n constructor(messageName: string) {\n this.messageName = messageName;\n }\n\n public getName(): string {\n return this.messageName;\n }\n\n public getMessages(): IMessageData[] {\n return this.data;\n }\n\n public pushMessage(data: object, sender: string): void {\n this.data.push(new MessageData(data, sender));\n }\n }\n\n /**\n * The peer to peer configuration.\n */\n let peerConfig: Peer.PeerJSOption = { debug: 1 };\n\n /**\n * The p2p client.\n */\n let peer: Peer<NetworkMessage> | null = null;\n\n /**\n * All connected p2p clients, keyed by their ID.\n */\n const connections = new Map<string, Peer.DataConnection<NetworkMessage>>();\n\n /**\n * Contains a map of message triggered by other p2p clients.\n * It is keyed by the event name.\n */\n const allMessages = new Map<string, IMessagesList>();\n\n /**\n * True if PeerJS is initialized and ready.\n */\n let ready = false;\n\n let _peerIdToConnectToOnceReady: string | null = null;\n\n /**\n * List of IDs of peers that just disconnected.\n */\n const justDisconnectedPeers: string[] = [];\n\n /**\n * List of IDs of peers that just remotely initiated a connection.\n */\n const justConnectedPeers: string[] = [];\n\n /**\n * The compression method used to compress data sent over the network.\n */\n let compressionMethod: CompressionMethod = 'none';\n export const setCompressionMethod = (method: CompressionMethod) => {\n compressionMethod = method;\n };\n\n /**\n * Helper function to compress data sent over the network.\n */\n async function compressData(data: object): Promise<Uint8Array | string> {\n if (compressionMethod === 'none') {\n // If no compression is used, we just stringify the data,\n // PeerJS will compress it to binary data.\n const jsonString = JSON.stringify(data);\n return jsonString;\n }\n\n const compressionStreamFormat =\n compressionMethod === 'cs:gzip' ? 'gzip' : 'deflate';\n\n const jsonString = JSON.stringify(data);\n const encoder = new TextEncoder();\n const array = encoder.encode(jsonString);\n\n // @ts-ignore - We checked that CompressionStream is available in the browser.\n const cs = new CompressionStream(compressionStreamFormat);\n const writer = cs.writable.getWriter();\n writer.write(array);\n writer.close();\n\n const compressedStream = cs.readable;\n const reader = compressedStream.getReader();\n const chunks: any[] = [];\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n const compressedData = new Uint8Array(\n chunks.reduce((acc, chunk) => acc.concat(Array.from(chunk)), [])\n );\n return compressedData;\n }\n\n /**\n * Helper function to decompress data received over the network.\n * It returns the parsed JSON object, if valid, or undefined.\n */\n async function decompressData(\n receivedData: Uint8Array | string\n ): Promise<object | undefined> {\n if (compressionMethod === 'none') {\n // If no compression is used, we just parse the data.\n if (typeof receivedData !== 'string') {\n logger.error(\n `Error while parsing message using compressionMethod ${compressionMethod}: received data is not a string.`\n );\n return;\n }\n\n try {\n const parsedData = JSON.parse(receivedData);\n return parsedData;\n } catch (e) {\n logger.error(`Error while parsing message: ${e.toString()}`);\n return;\n }\n }\n const compressionStreamFormat =\n compressionMethod === 'cs:gzip' ? 'gzip' : 'deflate';\n\n // @ts-ignore - We checked that DecompressionStream is available in the browser.\n const ds = new DecompressionStream(compressionStreamFormat);\n const writer = ds.writable.getWriter();\n writer.write(receivedData);\n writer.close();\n\n const decompressedStream = ds.readable;\n const reader = decompressedStream.getReader();\n const chunks: any[] = [];\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n chunks.push(value);\n }\n\n const decompressedData = new Uint8Array(\n chunks.reduce((acc, chunk) => acc.concat(Array.from(chunk)), [])\n );\n const decoder = new TextDecoder();\n const jsonStringData = decoder.decode(decompressedData); // Convert Uint8Array back to string\n try {\n const parsedData = JSON.parse(jsonStringData);\n return parsedData;\n } catch (e) {\n logger.error(`Error while parsing message: ${e.toString()}`);\n return;\n }\n }\n\n /**\n * Helper function to get the messages list for a given message name.\n */\n export const getOrCreateMessagesList = (\n messageName: string\n ): IMessagesList => {\n const messagesList = allMessages.get(messageName);\n if (messagesList) return messagesList;\n const newMessagesList = new MessagesList(messageName);\n allMessages.set(messageName, newMessagesList);\n return newMessagesList;\n };\n\n const _onReady = () => {\n ready = true;\n if (_peerIdToConnectToOnceReady) {\n connect(_peerIdToConnectToOnceReady);\n _peerIdToConnectToOnceReady = null;\n }\n };\n\n /**\n * Internal function called when a connection with a remote peer is initiated.\n * @param connection The DataConnection of the peer\n */\n const _onConnect = (connection: Peer.DataConnection<NetworkMessage>) => {\n connections.set(connection.peer, connection);\n connection.on('data', async (data) => {\n if (isValidNetworkMessage(data)) {\n const messagesList = getOrCreateMessagesList(data.messageName);\n const messageSender = connection.peer;\n const decompressedData = await decompressData(data.data);\n if (!decompressedData) return;\n\n messagesList.pushMessage(decompressedData, messageSender);\n }\n });\n\n // Close event is only for graceful disconnection,\n // but we want onDisconnect to trigger for any type of disconnection,\n // so we register a listener for both event types.\n connection.on('error', () => {\n _onDisconnect(connection.peer);\n });\n connection.on('close', () => {\n _onDisconnect(connection.peer);\n });\n connection.on('iceStateChanged', (state) => {\n if (state === 'disconnected') {\n _onDisconnect(connection.peer);\n }\n });\n\n // Regularly check for disconnection as the built in way is not reliable.\n (function disconnectChecker() {\n if (\n connection.peerConnection &&\n (connection.peerConnection.connectionState === 'failed' ||\n connection.peerConnection.connectionState === 'disconnected' ||\n connection.peerConnection.connectionState === 'closed')\n ) {\n _onDisconnect(connection.peer);\n } else {\n setTimeout(disconnectChecker, 1000);\n }\n })();\n };\n\n /**\n * Internal function called when a remote client disconnects.\n * @param connectionID The ID of the peer that disconnected.\n */\n const _onDisconnect = (connectionID: string) => {\n if (!connections.has(connectionID)) return;\n justDisconnectedPeers.push(connectionID);\n connections.delete(connectionID);\n };\n\n /**\n * Internal function called to initialize PeerJS after it\n * has been configured.\n */\n const initializePeerJS = (initOptions: PeerJSInitOptions = {}) => {\n if (peer !== null) return;\n peer = new Peer(peerConfig);\n peer.on('open', () => {\n _onReady();\n });\n peer.on('error', (error) => {\n // TODO: Support other error types listed in https://peerjs.com/docs/#peeron-error\n if (\n initOptions.onPeerUnavailable &&\n // @ts-ignore - PeerJS adds `type` on errors, but it doesn't show in their TS types.\n error.type === 'peer-unavailable'\n ) {\n logger.error('Peer is unavailable.');\n initOptions.onPeerUnavailable();\n } else {\n logger.error(\n // @ts-ignore - PeerJS adds `type` on errors, but it doesn't show in their TS types.\n `PeerJS error (${error.type || 'unknown'}):`,\n error\n );\n }\n });\n peer.on('connection', (connection) => {\n connection.on('open', () => {\n _onConnect(connection);\n justConnectedPeers.push(connection.peer);\n });\n });\n peer.on('close', () => {\n peer = null;\n initializePeerJS(initOptions);\n });\n peer.on('disconnected', peer.reconnect);\n };\n export const useDefaultBrokerServer = initializePeerJS;\n\n /**\n * Connects to another p2p client.\n * @param id - The other client's ID.\n */\n export const connect = (id: string) => {\n if (peer === null || !ready) {\n _peerIdToConnectToOnceReady = id;\n return;\n }\n const connection = peer.connect(id);\n connection.on('open', () => {\n _onConnect(connection);\n });\n };\n\n /**\n * Disconnects from all other p2p clients.\n */\n export const disconnectFromAllPeers = () => {\n for (const connection of connections.values()) connection.close();\n };\n\n /**\n * Send a message to a specific peer.\n * @param ids - The IDs of the clients to send the event to.\n * @param messageName - The event to trigger.\n * @param eventData - Additional data to send with the event.\n */\n export const sendDataTo = async (\n ids: string[],\n messageName: string,\n messageData: object\n ) => {\n if (!ids.length) return;\n\n const compressedData = await compressData(messageData);\n\n for (const id of ids) {\n const connection = connections.get(id);\n if (connection) {\n connection.send({\n messageName,\n data: compressedData,\n });\n }\n }\n };\n\n export const getAllMessagesMap = () => allMessages;\n\n /**\n * Connects to a custom broker server.\n * @param host The host of the broker server.\n * @param port The port of the broker server.\n * @param path The path (part of the url after the host) to the broker server.\n * @param key Optional password to connect to the broker server.\n * @param ssl Use ssl?\n * @param peerJSInitOptions @see PeerJSInitOptions\n */\n export const useCustomBrokerServer = (\n host: string,\n port: number,\n path: string,\n key: string,\n ssl: boolean,\n peerJSInitOptions: PeerJSInitOptions = {}\n ) => {\n Object.assign(peerConfig, {\n host,\n port,\n path,\n secure: ssl,\n // All servers have \"peerjs\" as default key\n key: key.length === 0 ? 'peerjs' : key,\n });\n initializePeerJS(peerJSInitOptions);\n };\n\n /**\n * Adds an ICE server candidate, and removes the default ones provided by PeerJs. Must be called before connecting to a broker.\n * @param urls The URL of the STUN/TURN server.\n * @param username An optional username to send to the server.\n * @param credential An optional password to send to the server.\n */\n export const useCustomICECandidate = (\n urls: string,\n username?: string,\n credential?: string\n ) => {\n peerConfig.config = peerConfig.config || {};\n peerConfig.config.iceServers = peerConfig.config.iceServers || [];\n peerConfig.config.iceServers.push({\n urls,\n username,\n credential,\n });\n };\n\n /**\n * Forces the usage of a relay (TURN) server, to avoid sharing IP addresses with the other peers.\n * @param shouldUseRelayServer Whether relay-only should be enabled or disabled.\n */\n export const forceUseRelayServer = (shouldUseRelayServer: boolean) => {\n peerConfig.config = peerConfig.config || {};\n peerConfig.config.iceTransportPolicy = shouldUseRelayServer\n ? 'relay'\n : 'all';\n };\n\n /**\n * Returns the own current peer ID.\n * @see Peer.id\n */\n export const getCurrentId = (): string => {\n if (peer === null) return '';\n return peer.id;\n };\n\n /**\n * Returns true once PeerJS finished initialization.\n * @see ready\n */\n export const isReady = () => ready;\n\n /**\n * Return peers that have disconnected in the frame.\n */\n export const getJustDisconnectedPeers = () => justDisconnectedPeers;\n\n /**\n * Returns the list of all currently connected peers.\n */\n export const getAllPeers = () => Array.from(connections.keys());\n\n gdjs.callbacksRuntimeScenePostEvents.push(() => {\n // Clear the list of messages at the end of the frame, assuming they've been all processed.\n for (const messagesList of allMessages.values()) {\n messagesList.getMessages().length = 0;\n }\n // Clear the list of just connected and disconnected peers.\n if (justDisconnectedPeers.length > 0) {\n justDisconnectedPeers.length = 0;\n }\n if (justConnectedPeers.length > 0) {\n justConnectedPeers.length = 0;\n }\n });\n }\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
*/
|
|
5
5
|
declare class EventEmitter<
|
|
6
6
|
EventTypes extends EventEmitter.ValidEventTypes = string | symbol,
|
|
7
|
-
Context extends any = any
|
|
7
|
+
Context extends any = any,
|
|
8
8
|
> {
|
|
9
9
|
static prefixed: string | boolean;
|
|
10
10
|
|
|
@@ -87,7 +87,7 @@ declare namespace EventEmitter {
|
|
|
87
87
|
export interface EventEmitterStatic {
|
|
88
88
|
new <
|
|
89
89
|
EventTypes extends ValidEventTypes = string | symbol,
|
|
90
|
-
Context = any
|
|
90
|
+
Context = any,
|
|
91
91
|
>(): EventEmitter<EventTypes, Context>;
|
|
92
92
|
}
|
|
93
93
|
|
|
@@ -110,13 +110,13 @@ declare namespace EventEmitter {
|
|
|
110
110
|
[K in keyof T]: T[K] extends (...args: any[]) => void
|
|
111
111
|
? Parameters<T[K]>
|
|
112
112
|
: T[K] extends any[]
|
|
113
|
-
|
|
114
|
-
|
|
113
|
+
? T[K]
|
|
114
|
+
: any[];
|
|
115
115
|
};
|
|
116
116
|
|
|
117
117
|
export type EventListener<
|
|
118
118
|
T extends ValidEventTypes,
|
|
119
|
-
K extends EventNames<T
|
|
119
|
+
K extends EventNames<T>,
|
|
120
120
|
> = T extends string | symbol
|
|
121
121
|
? (...args: any[]) => void
|
|
122
122
|
: (
|
|
@@ -125,7 +125,7 @@ declare namespace EventEmitter {
|
|
|
125
125
|
|
|
126
126
|
export type EventArgs<
|
|
127
127
|
T extends ValidEventTypes,
|
|
128
|
-
K extends EventNames<T
|
|
128
|
+
K extends EventNames<T>,
|
|
129
129
|
> = Parameters<EventListener<T, K>>;
|
|
130
130
|
|
|
131
131
|
export const EventEmitter: EventEmitterStatic;
|
|
@@ -170,9 +170,7 @@ declare namespace Peer {
|
|
|
170
170
|
validateId(id: string): boolean;
|
|
171
171
|
pack: any;
|
|
172
172
|
unpack: any;
|
|
173
|
-
chunk(
|
|
174
|
-
blob: Blob
|
|
175
|
-
): {
|
|
173
|
+
chunk(blob: Blob): {
|
|
176
174
|
__peerData: number;
|
|
177
175
|
n: number;
|
|
178
176
|
total: number;
|
|
@@ -267,7 +265,7 @@ declare namespace Peer {
|
|
|
267
265
|
};
|
|
268
266
|
abstract class BaseConnection<
|
|
269
267
|
T extends EventEmitter.ValidEventTypes,
|
|
270
|
-
TT
|
|
268
|
+
TT,
|
|
271
269
|
> extends EventEmitter<T & BaseConnectionEvents> {
|
|
272
270
|
readonly peer: string;
|
|
273
271
|
provider: Peer<TT>;
|