From 766c7f011f1b9e0bb18480c1b95a737b44bc29f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Pasteau?= <4895034+ClementPasteau@users.noreply.github.com> Date: Tue, 21 May 2024 18:29:36 +0200 Subject: [PATCH] Add more actions & conditions and fix automatic login --- Extensions/BBText/bbtextruntimeobject.ts | 3 +- .../BitmapText/bitmaptextruntimeobject.ts | 3 +- Extensions/Multiplayer/JsExtension.js | 111 +++++++++++++- Extensions/Multiplayer/messageManager.ts | 41 +++--- .../Multiplayer/multiplayercomponents.ts | 29 +++- .../multiplayerobjectruntimebehavior.ts | 41 ++++-- Extensions/Multiplayer/multiplayertools.ts | 138 ++++++++++++------ .../Multiplayer/tests/multiplayer.spec.js | 4 +- .../playerauthenticationtools.ts | 5 +- Extensions/TextObject/textruntimeobject.ts | 3 +- 10 files changed, 270 insertions(+), 108 deletions(-) diff --git a/Extensions/BBText/bbtextruntimeobject.ts b/Extensions/BBText/bbtextruntimeobject.ts index 36b28e53097a..1d98a526a2b3 100644 --- a/Extensions/BBText/bbtextruntimeobject.ts +++ b/Extensions/BBText/bbtextruntimeobject.ts @@ -43,8 +43,7 @@ namespace gdjs { */ export class BBTextRuntimeObject extends gdjs.RuntimeObject - implements gdjs.OpacityHandler - { + implements gdjs.OpacityHandler { _opacity: float; _text: string; diff --git a/Extensions/BitmapText/bitmaptextruntimeobject.ts b/Extensions/BitmapText/bitmaptextruntimeobject.ts index e37b64e0c93c..6fa907ec96e0 100644 --- a/Extensions/BitmapText/bitmaptextruntimeobject.ts +++ b/Extensions/BitmapText/bitmaptextruntimeobject.ts @@ -52,8 +52,7 @@ namespace gdjs { */ export class BitmapTextRuntimeObject extends gdjs.RuntimeObject - implements gdjs.TextContainer, gdjs.OpacityHandler, gdjs.Scalable - { + implements gdjs.TextContainer, gdjs.OpacityHandler, gdjs.Scalable { _opacity: float; _text: string; /** color in format [r, g, b], where each component is in the range [0, 255] */ diff --git a/Extensions/Multiplayer/JsExtension.js b/Extensions/Multiplayer/JsExtension.js index ee633e440bcc..0b955f3ffb37 100644 --- a/Extensions/Multiplayer/JsExtension.js +++ b/Extensions/Multiplayer/JsExtension.js @@ -59,6 +59,35 @@ module.exports = { .addIncludeFile('Extensions/Multiplayer/multiplayertools.js') .setFunctionName('gdjs.multiplayer.openLobbiesWindow'); + extension + .addAction( + 'ShowLobbiesWindowCloseAction', + _('Show (or hide) lobbies window close action'), + _( + 'Show or hide the close button in the lobbies window. The cross is shown by default.' + ), + _('Show lobbies window close action: _PARAM1_'), + '', + 'JsPlatform/Extensions/multiplayer.svg', + 'JsPlatform/Extensions/multiplayer.svg' + ) + .addCodeOnlyParameter('currentScene', '') + .addParameter('yesorno', _('Show close button'), '', false) + .setHelpPath('/all-features/multiplayer') + .getCodeExtraInformation() + .setIncludeFile('Extensions/P2P/A_peer.js') + .addIncludeFile('Extensions/P2P/B_p2ptools.js') + .addIncludeFile( + 'Extensions/PlayerAuthentication/playerauthenticationcomponents.js' + ) + .addIncludeFile( + 'Extensions/PlayerAuthentication/playerauthenticationtools.js' + ) + .addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js') + .addIncludeFile('Extensions/Multiplayer/messageManager.js') + .addIncludeFile('Extensions/Multiplayer/multiplayertools.js') + .setFunctionName('gdjs.multiplayer.showLobbiesCloseButton'); + extension .addAction( 'EndLobbyGame', @@ -91,7 +120,7 @@ module.exports = { 'SendMessage', _('Send custom message to other players'), _( - "Send a custom message to other players in the lobby, with an automatic retry system if it hasn't been received. Use with the condition 'Message has been received' to know when the message has been properly processed by the server." + "Send a custom message to other players in the lobby, with an automatic retry system if it hasn't been received. Use with the condition 'Message has been received' to know when the message has been properly processed by the host." ), _('Send message _PARAM0_ to other players with content _PARAM1_'), '', @@ -164,6 +193,30 @@ module.exports = { .addIncludeFile('Extensions/Multiplayer/multiplayertools.js') .setFunctionName('gdjs.multiplayer.hasGameJustStarted'); + extension + .addCondition( + 'IsGameRunning', + _('Lobby game is running'), + _('Check if the lobby game is running.'), + _('Lobby game is running'), + '', + 'JsPlatform/Extensions/multiplayer.svg', + 'JsPlatform/Extensions/multiplayer.svg' + ) + .getCodeExtraInformation() + .setIncludeFile('Extensions/P2P/A_peer.js') + .addIncludeFile('Extensions/P2P/B_p2ptools.js') + .addIncludeFile( + 'Extensions/PlayerAuthentication/playerauthenticationcomponents.js' + ) + .addIncludeFile( + 'Extensions/PlayerAuthentication/playerauthenticationtools.js' + ) + .addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js') + .addIncludeFile('Extensions/Multiplayer/messageManager.js') + .addIncludeFile('Extensions/Multiplayer/multiplayertools.js') + .setFunctionName('gdjs.multiplayer.isGameRunning'); + extension .addCondition( 'HasGameJustEnded', @@ -241,10 +294,10 @@ module.exports = { extension .addCondition( - 'isPlayerServer', - _('Player is server'), - _('Check if the player is the server. (Player 1 is the server)'), - _('Player is server'), + 'isPlayerHost', + _('Player is host'), + _('Check if the player is the host. (Player 1 is the host)'), + _('Player is host'), '', 'JsPlatform/Extensions/multiplayer.svg', 'JsPlatform/Extensions/multiplayer.svg' @@ -261,7 +314,7 @@ module.exports = { .addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js') .addIncludeFile('Extensions/Multiplayer/messageManager.js') .addIncludeFile('Extensions/Multiplayer/multiplayertools.js') - .setFunctionName('gdjs.multiplayer.isPlayerServer'); + .setFunctionName('gdjs.multiplayer.isPlayerHost'); extension .addStrExpression( @@ -460,12 +513,56 @@ module.exports = { .setFunctionName('setPlayerObjectOwnership') .setGetter('getPlayerObjectOwnership'); + behavior + .addScopedCondition( + 'IsObjectOwnedByCurrentPlayer', + _('Is object owned by current player'), + _( + 'Check if the object is owned by the current player, as a player or the host.' + ), + _('Object _PARAM0_ is owned by current player'), + '', + 'JsPlatform/Extensions/multiplayer.svg', + 'JsPlatform/Extensions/multiplayer.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter( + 'behavior', + _('Behavior'), + 'MultiplayerObjectBehavior', + false + ) + .markAsAdvanced() + .setFunctionName('isObjectOwnedByCurrentPlayer'); + + behavior + .addScopedAction( + 'TakeObjectOwnership', + _('Take ownership of object'), + _( + 'Take the ownership of the object. It will then be synchronized to other players, with the current player as the owner.' + ), + _('Take ownership of _PARAM0_'), + '', + 'JsPlatform/Extensions/multiplayer.svg', + 'JsPlatform/Extensions/multiplayer.svg' + ) + .addParameter('object', _('Object'), '', false) + .addParameter( + 'behavior', + _('Behavior'), + 'MultiplayerObjectBehavior', + false + ) + .markAsAdvanced() + .setFunctionName('takeObjectOwnership'); + behavior .addScopedAction( 'RemoveObjectOwnership', _('Remove object ownership'), _( - 'Remove the ownership of the object from the player. It will still be synchronized to other players, but the server owns it.' + 'Remove the ownership of the object from the player. It will still be synchronized to other players, but the host owns it.' ), _('Remove ownership of _PARAM0_'), '', diff --git a/Extensions/Multiplayer/messageManager.ts b/Extensions/Multiplayer/messageManager.ts index b4ba85ac8c2c..1c70be0c252b 100644 --- a/Extensions/Multiplayer/messageManager.ts +++ b/Extensions/Multiplayer/messageManager.ts @@ -108,7 +108,7 @@ namespace gdjs { maxNumberOfRetries?: number; messageRetryTime?: number; }) => { - if (!gdjs.multiplayer.isGameRunning) { + if (!gdjs.multiplayer.isGameRunning()) { // This can happen if objects are destroyed at the end of the scene. // We should not add expected messages in this case. return; @@ -418,17 +418,14 @@ namespace gdjs { const currentPlayerObjectOwnership = behavior.getPlayerObjectOwnership(); // Change is coherent if: const ownershipChangeIsCoherent = - // the object is changing ownership from the same owner the server knew about, + // the object is changing ownership from the same owner the host knew about, currentPlayerObjectOwnership === previousOwner || // the object is already owned by the new owner. (may have been changed by another player faster) currentPlayerObjectOwnership === newOwner; - if ( - gdjs.multiplayer.isPlayerServer() && - !ownershipChangeIsCoherent - ) { + if (gdjs.multiplayer.isPlayerHost() && !ownershipChangeIsCoherent) { // We received an ownership change message for an object which is in an unexpected state. // There may be some lag, and multiple ownership changes may have been sent by the other players. - // As the server, let's not change the ownership and let the player revert it. + // As the host, let's not change the ownership and let the player revert it. logger.warn( `Object ${objectName} with instance network ID ${instanceNetworkId} does not have the expected owner. Wanted to change from ${previousOwner} to ${newOwner}, but object has owner ${currentPlayerObjectOwnership}.` ); @@ -451,10 +448,10 @@ namespace gdjs { // Once the object ownership has changed, we need to acknowledge it to the player who sent this message. sendDataTo(messageSender, ownerChangedMessageName, {}); - // If we are the server, + // If we are the host, // so we need to relay the ownership change to others, // and expect an acknowledgment from them. - if (gdjs.multiplayer.isPlayerServer()) { + if (gdjs.multiplayer.isPlayerHost()) { const connectedPeerIds = gdjs.evtTools.p2p.getAllPeers(); // We don't need to send the message to the player who sent the ownership change message. const otherPeerIds = connectedPeerIds.filter( @@ -470,7 +467,7 @@ namespace gdjs { originalData: data, expectedMessageName: ownerChangedMessageName, otherPeerIds, - // As we are the server, we do not cancel the message if it times out. + // As we are the host, we do not cancel the message if it times out. shouldCancelMessageIfTimesOut: false, }); for (const peerId of otherPeerIds) { @@ -605,9 +602,9 @@ namespace gdjs { instanceNetworkId ] = messageInstanceClock; - // If we are are the server, + // If we are are the host, // we need to relay the position to others except the player who sent the update message. - if (gdjs.multiplayer.isPlayerServer()) { + if (gdjs.multiplayer.isPlayerHost()) { const connectedPeerIds = gdjs.evtTools.p2p.getAllPeers(); // We don't need to send the message to the player who sent the update message. for (const peerId of connectedPeerIds) { @@ -935,9 +932,9 @@ namespace gdjs { // Once the object is destroyed, we need to acknowledge it to the player who sent the destroy message. sendDataTo(messageSender, destroyedMessageName, {}); - // If we are the server, we need to relay the destruction to others. + // If we are the host, we need to relay the destruction to others. // And expect an acknowledgment from everyone else as well. - if (gdjs.multiplayer.isPlayerServer()) { + if (gdjs.multiplayer.isPlayerHost()) { const connectedPeerIds = gdjs.evtTools.p2p.getAllPeers(); // We don't need to send the message to the player who sent the destroy message. const otherPeerIds = connectedPeerIds.filter( @@ -1016,9 +1013,9 @@ namespace gdjs { sendDataTo(peerId, messageName, messageData); } - // If we are the server, we can consider this messaged as received + // If we are the host, we can consider this messaged as received // and add it to the list of custom messages to process on top of the messages received. - if (gdjs.multiplayer.isPlayerServer()) { + if (gdjs.multiplayer.isPlayerHost()) { const message = gdjs.evtTools.p2p.getEvent(messageName); message.pushData( new gdjs.evtTools.p2p.EventData( @@ -1119,9 +1116,9 @@ namespace gdjs { ); sendDataTo(messageSender, acknowledgmentMessageName, {}); - // If we are the server, + // If we are the host, // so we need to relay the message to others. - if (gdjs.multiplayer.isPlayerServer()) { + if (gdjs.multiplayer.isPlayerHost()) { // In the case of custom messages, we relay the message to all players, including the sender. // This allows the sender to process it the same way others would, when they receive the event. const connectedPeerIds = gdjs.evtTools.p2p.getAllPeers(); @@ -1186,8 +1183,8 @@ namespace gdjs { const handleUpdateSceneMessages = ( runtimeScene: gdjs.RuntimeScene ): void => { - // Only the server synchronizes the scene state. - if (!gdjs.multiplayer.isPlayerServer()) { + // Only the host synchronizes the scene state. + if (!gdjs.multiplayer.isPlayerHost()) { return; } const sceneNetworkSyncData = runtimeScene.getNetworkSyncData(); @@ -1285,8 +1282,8 @@ namespace gdjs { const handleUpdateGameMessages = ( runtimeScene: gdjs.RuntimeScene ): void => { - // Only the server synchronizes the global state. - if (!gdjs.multiplayer.isPlayerServer()) { + // Only the host synchronizes the global state. + if (!gdjs.multiplayer.isPlayerHost()) { return; } const gameNetworkSyncData = runtimeScene.getGame().getNetworkSyncData(); diff --git a/Extensions/Multiplayer/multiplayercomponents.ts b/Extensions/Multiplayer/multiplayercomponents.ts index e7901fc1dec8..957c80312437 100644 --- a/Extensions/Multiplayer/multiplayercomponents.ts +++ b/Extensions/Multiplayer/multiplayercomponents.ts @@ -9,6 +9,8 @@ namespace gdjs { const lobbiesIframeContainerId = 'lobbies-iframe-container'; const lobbiesIframeId = 'lobbies-iframe'; + let canLobbyBeClosed = true; + export const getDomElementContainer = ( runtimeScene: gdjs.RuntimeScene ): HTMLDivElement | null => { @@ -167,6 +169,10 @@ namespace gdjs { ); _closeContainer.appendChild(_close); + if (!canLobbyBeClosed) { + _closeContainer.style.visibility = 'hidden'; + } + const loaderContainer: HTMLDivElement = document.createElement('div'); loaderContainer.id = lobbiesLoaderContainerId; loaderContainer.style.display = 'flex'; @@ -349,20 +355,37 @@ namespace gdjs { rootContainer.remove(); }; - export const hideLobbiesCloseArrow = function ( + export const changeLobbiesWindowCloseActionVisibility = function ( + runtimeScene: gdjs.RuntimeScene, + canClose: boolean + ) { + canLobbyBeClosed = canClose; + + const closeContainer = getLobbiesCloseContainer(runtimeScene); + if (!closeContainer) { + return; + } + + closeContainer.style.visibility = canClose ? 'inherit' : 'hidden'; + }; + + export const hideLobbiesCloseButtonTemporarily = function ( runtimeScene: gdjs.RuntimeScene ) { + if (!canLobbyBeClosed) return; + const closeContainer = getLobbiesCloseContainer(runtimeScene); if (!closeContainer) { return; } - closeContainer.style.display = 'none'; + closeContainer.style.visibility = 'hidden'; + // There is a risk a player leaves the lobby before the end of the countdown, // so we show the close container again after 5 seconds in case this happens, // to allow the player to leave the lobby. setTimeout(() => { - closeContainer.style.display = 'flex'; + closeContainer.style.visibility = 'inherit'; }, 5000); }; diff --git a/Extensions/Multiplayer/multiplayerobjectruntimebehavior.ts b/Extensions/Multiplayer/multiplayerobjectruntimebehavior.ts index ccd172ba4e8f..db99ca01024f 100644 --- a/Extensions/Multiplayer/multiplayerobjectruntimebehavior.ts +++ b/Extensions/Multiplayer/multiplayerobjectruntimebehavior.ts @@ -11,7 +11,7 @@ namespace gdjs { */ export class MultiplayerObjectRuntimeBehavior extends gdjs.RuntimeBehavior { // Which player is the owner of the object. - // If 0, then the object is not owned by any player, so the server is the owner. + // If 0, then the object is not owned by any player, so the host is the owner. _playerNumber: number = 0; // The last time the object has been synchronized. // This is to avoid synchronizing the object too often, see _objectMaxTickRate. @@ -61,6 +61,7 @@ namespace gdjs { // and old messages are ignored. _clock: number = 0; _destroyInstanceTimeoutId: NodeJS.Timeout | null = null; + _timeBeforeDestroyingObjectWithoutNetworkIdInMs = 500; constructor( instanceContainer: gdjs.RuntimeInstanceContainer, @@ -75,8 +76,8 @@ namespace gdjs { // When a synchronized object is created, we assume it will be assigned a networkId quickly if: // - It is a new object created by the current player. -> will be assigned a networkId when sending the update message. // - It is an object created by another player. -> will be assigned a networkId when receiving the update message. - // There is a small risk that the object is created by us after we receive an update message from the server, - // ending up with 2 objects created, one with a networkId (from the server) and one without (from us). + // There is a small risk that the object is created by us after we receive an update message from the host, + // ending up with 2 objects created, one with a networkId (from the host) and one without (from us). // To handle this case and avoid having an object not synchronized, we set a timeout to destroy the object // if it has not been assigned a networkId after a short delay. @@ -87,7 +88,7 @@ namespace gdjs { ); owner.deleteFromScene(instanceContainer); } - }, 500); + }, this._timeBeforeDestroyingObjectWithoutNetworkIdInMs); } sendDataToPeersWithIncreasedClock(messageName: string, data: Object) { @@ -99,12 +100,12 @@ namespace gdjs { } } - isOwnerAsPlayerOrServer() { + isOwnerAsPlayerOrHost() { const currentPlayerNumber = gdjs.multiplayer.getPlayerNumber(); const isOwnerOfObject = currentPlayerNumber === this._playerNumber || // Player as owner. - (currentPlayerNumber === 1 && this._playerNumber === 0); // Server as owner. + (currentPlayerNumber === 1 && this._playerNumber === 0); // Host as owner. return isOwnerOfObject; } @@ -230,7 +231,7 @@ namespace gdjs { } doStepPostEvents() { - if (!this.isOwnerAsPlayerOrServer()) { + if (!this.isOwnerAsPlayerOrHost()) { return; } @@ -361,7 +362,7 @@ namespace gdjs { this._destroyInstanceTimeoutId = null; } - if (!this.isOwnerAsPlayerOrServer()) { + if (!this.isOwnerAsPlayerOrHost()) { return; } @@ -448,7 +449,7 @@ namespace gdjs { ); this._playerNumber = newPlayerNumber; if (newPlayerNumber !== gdjs.multiplayer.getPlayerNumber()) { - // If we are not the new owner, we should not send a message to the server to change the ownership. + // If we are not the new owner, we should not send a message to the host to change the ownership. // Just return and wait to receive an update message to reconcile this object. return; } @@ -458,9 +459,9 @@ namespace gdjs { const objectName = this.owner.getName(); if (instanceNetworkId) { - // When changing the ownership of an object with a networkId, we send a message to the server to ensure it is aware of the change, + // When changing the ownership of an object with a networkId, we send a message to the host to ensure it is aware of the change, // and can either accept it and broadcast it to other players, or reject it and do nothing with it. - // We expect an acknowledgment from the server, if not, we will retry and eventually revert the ownership. + // We expect an acknowledgment from the host, if not, we will retry and eventually revert the ownership. const { messageName, messageData, @@ -490,7 +491,7 @@ namespace gdjs { }, expectedMessageName: changeOwnerAcknowledgedMessageName, otherPeerIds, - // If we are not the server, we should revert the ownership if the server does not acknowledge the change. + // If we are not the host, we should revert the ownership if the host does not acknowledge the change. shouldCancelMessageIfTimesOut: currentPlayerNumber !== 1, }); } @@ -501,9 +502,9 @@ namespace gdjs { // We also update the ownership locally, so the object can be used immediately. // This is a prediction to allow snappy interactions. - // If we are player 1 or server, we will have the ownership immediately anyway. - // If we are another player, we will have the ownership as soon as the server acknowledges the change. - // If the server does not send an acknowledgment, we will revert the ownership. + // If we are player 1 or host, we will have the ownership immediately anyway. + // If we are another player, we will have the ownership as soon as the host acknowledges the change. + // If the host does not send an acknowledgment, we will revert the ownership. this._playerNumber = newPlayerNumber; // If we are the new owner, also send directly an update of the position, @@ -536,10 +537,18 @@ namespace gdjs { return this._playerNumber; } + isObjectOwnedByCurrentPlayer(): boolean { + return this.isOwnerAsPlayerOrHost(); + } + removeObjectOwnership() { - // 0 means the server is the owner. + // 0 means the host is the owner. this.setPlayerObjectOwnership(0); } + + takeObjectOwnership() { + this.setPlayerObjectOwnership(gdjs.multiplayer.getPlayerNumber()); + } } gdjs.registerBehavior( 'Multiplayer::MultiplayerObjectBehavior', diff --git a/Extensions/Multiplayer/multiplayertools.ts b/Extensions/Multiplayer/multiplayertools.ts index 020b5f8987fa..35b0f1020e4e 100644 --- a/Extensions/Multiplayer/multiplayertools.ts +++ b/Extensions/Multiplayer/multiplayertools.ts @@ -7,7 +7,12 @@ namespace gdjs { /** Set to true in testing to avoid relying on the multiplayer extension. */ export let disableMultiplayerForTesting = false; + let _isGameRegistered: boolean | null = null; + let _isCheckingIfGameIsRegistered = false; + let _isWaitingForLoginCallback = false; + let _hasGameJustStarted = false; + export let _isGameRunning = false; let _hasGameJustEnded = false; let _lobbyId: string | null = null; let _connectionId: string | null = null; @@ -31,7 +36,6 @@ namespace gdjs { let _heartbeatInterval: NodeJS.Timeout | null = null; export let playerNumber: number | null = null; - export let isGameRunning = false; gdjs.registerRuntimeScenePreEventsCallback( (runtimeScene: gdjs.RuntimeScene) => { @@ -115,6 +119,8 @@ namespace gdjs { */ export const hasGameJustStarted = () => _hasGameJustStarted; + export const isGameRunning = () => _isGameRunning; + /** * Returns true if the game has just ended, * useful to switch back to to the main menu. @@ -146,9 +152,9 @@ namespace gdjs { }; /** - * Returns true if the player is the server in the lobby. Here, player 1. + * Returns true if the player is the host in the lobby. Here, player 1. */ - export const isPlayerServer = () => { + export const isPlayerHost = () => { return playerNumber === 1; }; @@ -629,7 +635,7 @@ namespace gdjs { ); // Prevent the player from leaving the lobby while the game is starting. - multiplayerComponents.hideLobbiesCloseArrow(runtimeScene); + multiplayerComponents.hideLobbiesCloseButtonTemporarily(runtimeScene); }; /** @@ -638,7 +644,7 @@ namespace gdjs { */ const handleGameStartedEvent = function (runtimeScene: gdjs.RuntimeScene) { _hasGameJustStarted = true; - isGameRunning = true; + _isGameRunning = true; _lobbyOnGameStart = _lobby; removeLobbiesContainer(runtimeScene); focusOnGame(runtimeScene); @@ -650,7 +656,7 @@ namespace gdjs { */ const handleGameEndedEvent = function () { _hasGameJustEnded = true; - isGameRunning = false; + _isGameRunning = false; // Disconnect from any P2P connections. gdjs.evtTools.p2p.disconnectFromAllPeers(); @@ -860,13 +866,56 @@ namespace gdjs { /** * Action to display the lobbies window to the user. */ - export const openLobbiesWindow = function ( + export const openLobbiesWindow = async ( runtimeScene: gdjs.RuntimeScene - ) { + ) => { if (isLobbiesWindowOpen(runtimeScene)) { return; } + const _gameId = gdjs.projectData.properties.projectUuid; + if (!_gameId) { + handleLobbiesError( + runtimeScene, + 'The game ID is missing, the lobbies window cannot be opened.' + ); + return; + } + + logger.info(_isCheckingIfGameIsRegistered, _isWaitingForLoginCallback); + + if (_isCheckingIfGameIsRegistered || _isWaitingForLoginCallback) { + // The action is called multiple times, let's prevent that. + return; + } + + if (_isGameRegistered === null) { + logger.info('Checking if the game is registered.'); + _isCheckingIfGameIsRegistered = true; + try { + const isGameRegistered = await checkIfGameIsRegistered( + runtimeScene.getGame(), + _gameId + ); + logger.info('Game registration:', isGameRegistered); + _isGameRegistered = isGameRegistered; + } catch (error) { + _isGameRegistered = false; + logger.error( + 'Error while checking if the game is registered:', + error + ); + handleLobbiesError( + runtimeScene, + 'Error while checking if the game is registered.' + ); + logger.error(error); + return; + } finally { + _isCheckingIfGameIsRegistered = false; + } + } + // Create the lobbies container for the player to wait. const domElementContainer = runtimeScene .getGame() @@ -884,21 +933,14 @@ namespace gdjs { removeLobbiesContainer(runtimeScene); }; - const _gameId = gdjs.projectData.properties.projectUuid; - if (!_gameId) { - handleLobbiesError( - runtimeScene, - 'The game ID is missing, the lobbies window cannot be opened.' - ); - return; - } - const playerId = gdjs.playerAuthentication.getUserId(); const playerToken = gdjs.playerAuthentication.getUserToken(); - if (!playerId || !playerToken) { + if (_isGameRegistered && (!playerId || !playerToken)) { + _isWaitingForLoginCallback = true; gdjs.playerAuthentication.openAuthenticationWindow(runtimeScene); // Create a callback to open the lobbies window once the player is connected. gdjs.playerAuthentication.setLoginCallback(() => { + _isWaitingForLoginCallback = false; openLobbiesWindow(runtimeScene); }); return; @@ -911,37 +953,27 @@ namespace gdjs { // If the game is registered, open the lobbies window. // Otherwise, open the window indicating that the game is not registered. - checkIfGameIsRegistered(runtimeScene.getGame(), _gameId) - .then((isGameRegistered) => { - const electron = runtimeScene.getGame().getRenderer().getElectron(); - const wikiOpenAction = electron - ? () => - electron.shell.openExternal( - 'https://wiki.gdevelop.io/gdevelop5/publishing/web' - ) - : () => - window.open( - 'https://wiki.gdevelop.io/gdevelop5/publishing/web', - '_blank' - ); - - multiplayerComponents.addTextsToLoadingContainer( - runtimeScene, - isGameRegistered, - wikiOpenAction - ); + const electron = runtimeScene.getGame().getRenderer().getElectron(); + const wikiOpenAction = electron + ? () => + electron.shell.openExternal( + 'https://wiki.gdevelop.io/gdevelop5/publishing/web' + ) + : () => + window.open( + 'https://wiki.gdevelop.io/gdevelop5/publishing/web', + '_blank' + ); - if (isGameRegistered) { - openLobbiesIframe(runtimeScene, _gameId); - } - }) - .catch((error) => { - handleLobbiesError( - runtimeScene, - 'Error while checking if the game is registered.' - ); - logger.error(error); - }); + multiplayerComponents.addTextsToLoadingContainer( + runtimeScene, + _isGameRegistered, + wikiOpenAction + ); + + if (_isGameRegistered) { + openLobbiesIframe(runtimeScene, _gameId); + } }; /** @@ -956,6 +988,16 @@ namespace gdjs { return !!lobbiesRootContainer; }; + export const showLobbiesCloseButton = function ( + runtimeScene: gdjs.RuntimeScene, + visible: boolean + ) { + multiplayerComponents.changeLobbiesWindowCloseActionVisibility( + runtimeScene, + visible + ); + }; + /** * Remove the container displaying the lobbies window and the callback. */ diff --git a/Extensions/Multiplayer/tests/multiplayer.spec.js b/Extensions/Multiplayer/tests/multiplayer.spec.js index c4f3b7ed34d1..9fcb2002e637 100644 --- a/Extensions/Multiplayer/tests/multiplayer.spec.js +++ b/Extensions/Multiplayer/tests/multiplayer.spec.js @@ -247,12 +247,12 @@ describe('Multiplayer', () => { beforeEach(() => { _originalP2pIfAny = gdjs.evtTools.p2p; gdjs.multiplayer.disableMultiplayerForTesting = false; - gdjs.multiplayer.isGameRunning = true; + gdjs.multiplayer._isGameRunning = true; }); afterEach(() => { gdjs.evtTools.p2p = _originalP2pIfAny; gdjs.multiplayer.disableMultiplayerForTesting = true; - gdjs.multiplayer.isGameRunning = false; + gdjs.multiplayer._isGameRunning = false; }); it('synchronizes scene/global variables from the server to other players', () => { diff --git a/Extensions/PlayerAuthentication/playerauthenticationtools.ts b/Extensions/PlayerAuthentication/playerauthenticationtools.ts index c467f9bcc7ea..fd76f9387c36 100644 --- a/Extensions/PlayerAuthentication/playerauthenticationtools.ts +++ b/Extensions/PlayerAuthentication/playerauthenticationtools.ts @@ -378,6 +378,7 @@ namespace gdjs { userToken: string; }) => { saveAuthKeyToStorage({ userId, username, userToken }); + _loginCallbacks.forEach((callback) => callback()); cleanUpAuthWindowAndCallbacks(runtimeScene); removeAuthenticationBanner(runtimeScene); @@ -396,10 +397,6 @@ namespace gdjs { domElementContainer, _username || 'Anonymous' ); - - _loginCallbacks.forEach((callback) => callback()); - // Clear the callbacks, as they are only called once. - _loginCallbacks = []; }; /** diff --git a/Extensions/TextObject/textruntimeobject.ts b/Extensions/TextObject/textruntimeobject.ts index 3385796972ef..4b536031165b 100644 --- a/Extensions/TextObject/textruntimeobject.ts +++ b/Extensions/TextObject/textruntimeobject.ts @@ -71,8 +71,7 @@ namespace gdjs { */ export class TextRuntimeObject extends gdjs.RuntimeObject - implements gdjs.TextContainer, gdjs.OpacityHandler - { + implements gdjs.TextContainer, gdjs.OpacityHandler { _characterSize: number; _fontName: string; _bold: boolean;