mirror of
https://github.com/4ian/GDevelop.git
synced 2025-10-15 10:19:04 +00:00
Add an action to join a lobby in one-click (#7352)
This commit is contained in:
@@ -31,6 +31,128 @@ module.exports = {
|
||||
.addInstructionOrExpressionGroupMetadata(_('Multiplayer'))
|
||||
.setIcon('JsPlatform/Extensions/multiplayer.svg');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'QuickJoinLobby',
|
||||
_('Join the next available lobby'),
|
||||
_(
|
||||
'Join the next available lobby. The player will join the game instantly if this is possible.'
|
||||
),
|
||||
_('Join the next available lobby'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.addParameter(
|
||||
'yesorno',
|
||||
_('Display loader while searching for a lobby.'),
|
||||
'',
|
||||
true
|
||||
)
|
||||
.setDefaultValue('yes')
|
||||
.addParameter(
|
||||
'yesorno',
|
||||
_('Display game lobbies if no lobby can be joined directly.'),
|
||||
'',
|
||||
true
|
||||
)
|
||||
.setDefaultValue('yes')
|
||||
.setHelpPath('/all-features/multiplayer')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayer.authenticateAndQuickJoinLobby');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'IsSearchingForLobbyToJoin',
|
||||
_('Is searching for a lobby to join'),
|
||||
_('Is searching for a lobby to join.'),
|
||||
_('Is searching for a lobby to join'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.setHelpPath('/all-features/multiplayer')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayer.isSearchingForLobbyToJoin');
|
||||
|
||||
extension
|
||||
.addCondition(
|
||||
'QuickJoinJustFailed',
|
||||
_('Quick join failed to join a lobby'),
|
||||
_('Quick join failed to join a lobby.'),
|
||||
_('Quick join failed to join a lobby'),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg',
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.addCodeOnlyParameter('currentScene', '')
|
||||
.setHelpPath('/all-features/multiplayer')
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayer.hasQuickJoinJustFailed');
|
||||
|
||||
extension
|
||||
.addStrExpression(
|
||||
'QuickJoinFailureReason',
|
||||
_('Quick join action failure reason'),
|
||||
_(
|
||||
"Returns the reason why the Quick join action failed. It can either be 'FULL' if all lobbies were occupied, 'NOT_ENOUGH_PLAYERS' if the lobby's configuration requires more than 1 player to start the game and no other players were available. It can also take the value 'UNKNOWN'."
|
||||
),
|
||||
_('Lobbies'),
|
||||
'JsPlatform/Extensions/multiplayer.svg'
|
||||
)
|
||||
.getCodeExtraInformation()
|
||||
.setIncludeFile('Extensions/Multiplayer/peer.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/peerJsHelper.js')
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
|
||||
)
|
||||
.addIncludeFile(
|
||||
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
|
||||
)
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayercomponents.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/messageManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayerVariablesManager.js')
|
||||
.addIncludeFile('Extensions/Multiplayer/multiplayertools.js')
|
||||
.setFunctionName('gdjs.multiplayer.getQuickJoinFailureReason');
|
||||
|
||||
extension
|
||||
.addAction(
|
||||
'OpenGameLobbies',
|
||||
|
@@ -1945,12 +1945,12 @@ namespace gdjs {
|
||||
};
|
||||
|
||||
const heartbeatMessageNamePrefix = '#heartbeat';
|
||||
const heartbeastMessageRegex = /#heartbeat#(.+)/;
|
||||
const heartbeatMessageRegex = /#heartbeat#(.+)/;
|
||||
const createHeartbeatMessage = (): {
|
||||
messageName: string;
|
||||
messageData: any;
|
||||
} => {
|
||||
// If we create the heartbeat meassage, we are the host,
|
||||
// If we create the heartbeat message, we are the host.
|
||||
// Ensure our player number is correctly set when the first heartbeat is sent.
|
||||
_playersInfo[gdjs.multiplayer.getCurrentPlayerNumber()] = {
|
||||
ping: 0, // we are the host, so we don't need to compute the ping.
|
||||
@@ -2028,7 +2028,7 @@ namespace gdjs {
|
||||
messages.forEach((message) => {
|
||||
const messageData = message.getData();
|
||||
const messageSender = message.getSender();
|
||||
const matches = heartbeastMessageRegex.exec(messageName);
|
||||
const matches = heartbeatMessageRegex.exec(messageName);
|
||||
if (!matches) {
|
||||
return;
|
||||
}
|
||||
|
@@ -1,6 +1,7 @@
|
||||
namespace gdjs {
|
||||
const logger = new gdjs.Logger('Multiplayer');
|
||||
export namespace multiplayerComponents {
|
||||
const loaderContainerId = 'loader-container';
|
||||
const lobbiesRootContainerId = 'lobbies-root-container';
|
||||
const lobbiesFrameContainerId = 'lobbies-frame-container';
|
||||
const lobbiesCloseContainerId = 'lobbies-close-container';
|
||||
@@ -117,6 +118,66 @@ namespace gdjs {
|
||||
return lobbiesIframe;
|
||||
};
|
||||
|
||||
export const displayLoader = (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
yesOrNo: boolean
|
||||
) => {
|
||||
const domElementContainer = getDomElementContainer(runtimeScene);
|
||||
if (!domElementContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (yesOrNo) {
|
||||
const loaderContainer: HTMLDivElement = document.createElement('div');
|
||||
loaderContainer.id = loaderContainerId;
|
||||
loaderContainer.style.backgroundColor = '#000000';
|
||||
loaderContainer.style.display = 'flex';
|
||||
loaderContainer.style.height = '100%';
|
||||
loaderContainer.style.width = '100%';
|
||||
loaderContainer.style.justifyContent = 'center';
|
||||
loaderContainer.style.alignItems = 'center';
|
||||
loaderContainer.style.position = 'relative';
|
||||
loaderContainer.style.zIndex = '2';
|
||||
const loader = document.createElement('img');
|
||||
loader.setAttribute('width', '50px');
|
||||
loader.setAttribute(
|
||||
'src',
|
||||
'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAyNCAyNCI+CjxjaXJjbGUgb3BhY2l0eT0nMC4yNScgY3g9IjEyIiBjeT0iMTIiIHI9IjEwIiBzdHJva2U9IiNGRkZGRkYiIHN0cm9rZS13aWR0aD0iNCI+PC9jaXJjbGU+CjxwYXRoIG9wYWNpdHk9JzAuNzUnIGZpbGw9IiNGRkZGRkYiIGQ9Ik00IDEyYTggOCAwIDAxOC04VjBDNS4zNzMgMCAwIDUuMzczIDAgMTJoNHptMiA1LjI5MUE3Ljk2MiA3Ljk2MiAwIDAxNCAxMkgwYzAgMy4wNDIgMS4xMzUgNS44MjQgMyA3LjkzOGwzLTIuNjQ3eiI+PC9wYXRoPgo8L3N2Zz4='
|
||||
);
|
||||
try {
|
||||
loader.animate(
|
||||
[{ transform: 'rotate(0deg)' }, { transform: 'rotate(359deg)' }],
|
||||
{
|
||||
duration: 3000,
|
||||
iterations: Infinity,
|
||||
}
|
||||
);
|
||||
} catch {
|
||||
logger.warn('Animation not supported, loader will be fixed.');
|
||||
}
|
||||
loaderContainer.appendChild(loader);
|
||||
if (
|
||||
domElementContainer.children &&
|
||||
domElementContainer.children.length > 0
|
||||
) {
|
||||
domElementContainer.insertBefore(
|
||||
loaderContainer,
|
||||
domElementContainer.children[0]
|
||||
);
|
||||
} else {
|
||||
domElementContainer.appendChild(loaderContainer);
|
||||
}
|
||||
} else {
|
||||
const loaderContainer = domElementContainer.querySelector(
|
||||
`#${loaderContainerId}`
|
||||
);
|
||||
if (!loaderContainer) return;
|
||||
try {
|
||||
domElementContainer.removeChild(loaderContainer);
|
||||
} catch {}
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates a DOM element that will contain the loader or a message if the game is not registered,
|
||||
* and adds it to the dom container.
|
||||
|
@@ -17,6 +17,17 @@ namespace gdjs {
|
||||
}[];
|
||||
};
|
||||
|
||||
type Lobby = {
|
||||
id: string;
|
||||
status: 'waiting' | 'starting' | 'playing' | 'migrating' | 'migrated';
|
||||
};
|
||||
|
||||
type QuickJoinLobbyResponse =
|
||||
| { status: 'join-game'; lobby: Lobby }
|
||||
| { status: 'join-lobby'; lobby: Lobby }
|
||||
| { status: 'not-enough-players' }
|
||||
| { status: 'full' };
|
||||
|
||||
const getTimeNow =
|
||||
window.performance && typeof window.performance.now === 'function'
|
||||
? window.performance.now.bind(window.performance)
|
||||
@@ -90,6 +101,12 @@ namespace gdjs {
|
||||
let _hasLobbyGameJustStarted = false;
|
||||
export let _isLobbyGameRunning = false;
|
||||
let _hasLobbyGameJustEnded = false;
|
||||
let _quickJoinLobbyJustFailed = false;
|
||||
let _quickJoinLobbyFailureReason:
|
||||
| 'FULL'
|
||||
| 'NOT_ENOUGH_PLAYERS'
|
||||
| 'UNKNOWN'
|
||||
| null = null;
|
||||
let _lobbyId: string | null = null;
|
||||
let _connectionId: string | null = null;
|
||||
|
||||
@@ -98,6 +115,13 @@ namespace gdjs {
|
||||
let _lobbyChangeHostRequestInitiatedAt: number | null = null;
|
||||
let _isChangingHost = false;
|
||||
let _lobbyNewHostPickedAt: number | null = null;
|
||||
let _actionAfterJoiningLobby:
|
||||
| 'OPEN_LOBBY_PAGE'
|
||||
| 'JOIN_GAME'
|
||||
| 'START_GAME'
|
||||
| null = null;
|
||||
let _isQuickJoiningOrStartingAGame = false;
|
||||
let _lastQuickJoinRequestDoneAt: number | null = null;
|
||||
|
||||
// Communication methods.
|
||||
let _lobbiesMessageCallback: ((event: MessageEvent) => void) | null = null;
|
||||
@@ -206,6 +230,7 @@ namespace gdjs {
|
||||
|
||||
_hasLobbyGameJustStarted = false;
|
||||
_hasLobbyGameJustEnded = false;
|
||||
_quickJoinLobbyJustFailed = false;
|
||||
});
|
||||
|
||||
const getLobbiesWindowUrl = ({
|
||||
@@ -253,6 +278,11 @@ namespace gdjs {
|
||||
if (playerToken) {
|
||||
url.searchParams.set('playerToken', playerToken);
|
||||
}
|
||||
const platformInfo = runtimeGame.getPlatformInfo();
|
||||
url.searchParams.set(
|
||||
'scm',
|
||||
platformInfo.supportedCompressionMethods.join(',')
|
||||
);
|
||||
// Increment this value when a new feature is introduced so we can
|
||||
// adapt the interface of the lobbies.
|
||||
url.searchParams.set('multiplayerVersion', '2');
|
||||
@@ -592,8 +622,16 @@ namespace gdjs {
|
||||
logger.error('Malformed message received');
|
||||
return;
|
||||
}
|
||||
|
||||
handlePeerIdEvent({ peerId, compressionMethod });
|
||||
const retryData = { times: 2, delayInMs: 500 };
|
||||
try {
|
||||
gdjs.evtTools.network.retryIfFailed(retryData, async () => {
|
||||
handlePeerIdEvent({ peerId, compressionMethod });
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Handling peerId message from websocket failed (after {${retryData.times}} times with a delay of ${retryData.delayInMs}ms). Not trying anymore.`
|
||||
);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -634,6 +672,17 @@ namespace gdjs {
|
||||
};
|
||||
};
|
||||
|
||||
const onPeerUnavailable = (runtimeScene: gdjs.RuntimeScene) => {
|
||||
gdjs.multiplayerComponents.displayConnectionErrorNotification(
|
||||
runtimeScene
|
||||
);
|
||||
handleLeaveLobbyEvent();
|
||||
_actionAfterJoiningLobby = null;
|
||||
_quickJoinLobbyFailureReason = null;
|
||||
if (_isQuickJoiningOrStartingAGame)
|
||||
onLobbyQuickJoinFinished(runtimeScene);
|
||||
};
|
||||
|
||||
const handleConnectionIdReceived = function ({
|
||||
runtimeScene,
|
||||
connectionId,
|
||||
@@ -679,10 +728,13 @@ namespace gdjs {
|
||||
brokerServerConfig.port,
|
||||
brokerServerConfig.path,
|
||||
brokerServerConfig.key,
|
||||
brokerServerConfig.secure
|
||||
brokerServerConfig.secure,
|
||||
{ onPeerUnavailable: () => onPeerUnavailable(runtimeScene) }
|
||||
);
|
||||
} else {
|
||||
gdjs.multiplayerPeerJsHelper.useDefaultBrokerServer();
|
||||
gdjs.multiplayerPeerJsHelper.useDefaultBrokerServer({
|
||||
onPeerUnavailable: () => onPeerUnavailable(runtimeScene),
|
||||
});
|
||||
}
|
||||
|
||||
_connectionId = connectionId;
|
||||
@@ -690,6 +742,28 @@ namespace gdjs {
|
||||
// We save the lobbyId here as this is the moment when the player is really connected to the lobby.
|
||||
_lobbyId = lobbyId;
|
||||
|
||||
if (_actionAfterJoiningLobby === 'OPEN_LOBBY_PAGE') {
|
||||
openLobbiesWindow(runtimeScene);
|
||||
onLobbyQuickJoinFinished(runtimeScene);
|
||||
return;
|
||||
} else if (_actionAfterJoiningLobby === 'JOIN_GAME') {
|
||||
handleJoinGameMessage();
|
||||
return;
|
||||
} else if (_actionAfterJoiningLobby === 'START_GAME') {
|
||||
const retryData = { times: 2, delayInMs: 500 };
|
||||
try {
|
||||
gdjs.evtTools.network.retryIfFailed(retryData, async () => {
|
||||
sendPeerId();
|
||||
handleStartGameMessage();
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Sending of peerId message from websocket failed (after {${retryData.times}} times with a delay of ${retryData.delayInMs}ms). Not trying anymore.`
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Then we inform the lobbies window that the player has joined.
|
||||
const lobbiesIframe = gdjs.multiplayerComponents.getLobbiesIframe(
|
||||
runtimeScene
|
||||
@@ -818,6 +892,8 @@ namespace gdjs {
|
||||
}),
|
||||
dev: isUsingGDevelopDevelopmentEnvironment,
|
||||
});
|
||||
// TODO: if 404, there's chance that it means the lobby is now closed. Display a message
|
||||
// to the player?
|
||||
} catch (error) {
|
||||
logger.error('Error while sending heartbeat, retrying:', error);
|
||||
try {
|
||||
@@ -873,6 +949,8 @@ namespace gdjs {
|
||||
logger.info('Lobby game has started.');
|
||||
// In case we're joining an existing lobby, read the saved messages to catch-up with the game state.
|
||||
gdjs.multiplayerMessageManager.handleSavedUpdateMessages(runtimeScene);
|
||||
if (_isQuickJoiningOrStartingAGame)
|
||||
onLobbyQuickJoinFinished(runtimeScene);
|
||||
_isReadyToSendOrReceiveGameUpdateMessages = true;
|
||||
_hasLobbyGameJustStarted = true;
|
||||
_isLobbyGameRunning = true;
|
||||
@@ -926,7 +1004,7 @@ namespace gdjs {
|
||||
logger.error(
|
||||
'No peerId found, the player does not seem connected to the broker server.'
|
||||
);
|
||||
return;
|
||||
throw new Error('Missing player peerId.');
|
||||
}
|
||||
|
||||
if (currentPeerId === peerId) {
|
||||
@@ -990,10 +1068,15 @@ namespace gdjs {
|
||||
const handleJoinGameMessage = function () {
|
||||
if (!_websocket) {
|
||||
logger.error(
|
||||
'No connection to send the start countdown message. Are you connected to a lobby?'
|
||||
'No connection to send the join game message. Are you connected to a lobby?'
|
||||
);
|
||||
return;
|
||||
}
|
||||
// TODO: When the message is sent, it is expected to then receive a "peerId" message
|
||||
// from the websocket. This "peerId" message might not be sent for different reasons.
|
||||
// Should there be a security that checks if the "peerId" message has been received
|
||||
// in the next 10s or something more global that checks the lobby status after the player
|
||||
// has committed to open a connection with it?
|
||||
|
||||
_websocket.send(
|
||||
JSON.stringify({
|
||||
@@ -1388,7 +1471,7 @@ namespace gdjs {
|
||||
logger.error(
|
||||
"No peerId found, the player doesn't seem connected to the broker server."
|
||||
);
|
||||
return;
|
||||
throw new Error('Missing player peerId.');
|
||||
}
|
||||
|
||||
_websocket.send(
|
||||
@@ -1433,7 +1516,7 @@ namespace gdjs {
|
||||
if (!event.data.lobbyId) {
|
||||
throw new Error('Malformed message.');
|
||||
}
|
||||
|
||||
_actionAfterJoiningLobby = null;
|
||||
handleJoinLobbyEvent(runtimeScene, event.data.lobbyId);
|
||||
break;
|
||||
}
|
||||
@@ -1519,6 +1602,162 @@ namespace gdjs {
|
||||
);
|
||||
};
|
||||
|
||||
const onLobbyQuickJoinFinished = (runtimeScene: gdjs.RuntimeScene) => {
|
||||
_isQuickJoiningOrStartingAGame = false;
|
||||
_actionAfterJoiningLobby = null;
|
||||
gdjs.multiplayerComponents.displayLoader(runtimeScene, false);
|
||||
};
|
||||
|
||||
const quickJoinLobby = async (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
displayLoader: boolean,
|
||||
openLobbiesPageIfFailure: boolean
|
||||
) => {
|
||||
if (_isQuickJoiningOrStartingAGame) return;
|
||||
const _gameId = gdjs.projectData.properties.projectUuid;
|
||||
if (!_gameId) {
|
||||
handleLobbiesError(
|
||||
runtimeScene,
|
||||
'The game ID is missing, the quick join lobby action cannot continue.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
_quickJoinLobbyFailureReason = null;
|
||||
_isQuickJoiningOrStartingAGame = true;
|
||||
if (displayLoader) {
|
||||
gdjs.multiplayerComponents.displayLoader(runtimeScene, true);
|
||||
}
|
||||
|
||||
const quickJoinLobbyRelativeUrl = `/play/game/${_gameId}/public-lobby/action/quick-join`;
|
||||
const platformInfo = runtimeScene.getGame().getPlatformInfo();
|
||||
|
||||
try {
|
||||
const quickJoinLobbyResponse: QuickJoinLobbyResponse = await gdjs.evtTools.network.retryIfFailed(
|
||||
{ times: 2 },
|
||||
() =>
|
||||
fetchAsPlayer({
|
||||
relativeUrl: quickJoinLobbyRelativeUrl,
|
||||
method: 'POST',
|
||||
dev: isUsingGDevelopDevelopmentEnvironment,
|
||||
body: JSON.stringify({
|
||||
isPreview: runtimeScene.getGame().isPreview(),
|
||||
gameVersion: runtimeScene.getGame().getGameData().properties
|
||||
.version,
|
||||
supportedCompressionMethods:
|
||||
platformInfo.supportedCompressionMethods,
|
||||
}),
|
||||
})
|
||||
);
|
||||
|
||||
if (
|
||||
quickJoinLobbyResponse.status === 'full' ||
|
||||
quickJoinLobbyResponse.status === 'not-enough-players'
|
||||
) {
|
||||
_quickJoinLobbyJustFailed = true;
|
||||
_quickJoinLobbyFailureReason =
|
||||
quickJoinLobbyResponse.status === 'full'
|
||||
? 'FULL'
|
||||
: 'NOT_ENOUGH_PLAYERS';
|
||||
onLobbyQuickJoinFinished(runtimeScene);
|
||||
if (openLobbiesPageIfFailure) {
|
||||
openLobbiesWindow(runtimeScene);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (quickJoinLobbyResponse.status === 'join-game') {
|
||||
if (quickJoinLobbyResponse.lobby.status === 'waiting') {
|
||||
_actionAfterJoiningLobby = 'START_GAME';
|
||||
} else if (quickJoinLobbyResponse.lobby.status === 'playing') {
|
||||
_actionAfterJoiningLobby = 'JOIN_GAME';
|
||||
} else {
|
||||
throw new Error(
|
||||
`Lobby in wrong status: ${quickJoinLobbyResponse.status}`
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if (_connectionId) {
|
||||
// Already connected to a lobby.
|
||||
onLobbyQuickJoinFinished(runtimeScene);
|
||||
openLobbiesWindow(runtimeScene);
|
||||
return;
|
||||
} else {
|
||||
_actionAfterJoiningLobby = 'OPEN_LOBBY_PAGE';
|
||||
}
|
||||
}
|
||||
handleJoinLobbyEvent(runtimeScene, quickJoinLobbyResponse.lobby.id);
|
||||
} catch (error) {
|
||||
logger.error('An error occurred while joining a lobby:', error);
|
||||
_quickJoinLobbyJustFailed = true;
|
||||
_quickJoinLobbyFailureReason = 'UNKNOWN';
|
||||
onLobbyQuickJoinFinished(runtimeScene);
|
||||
if (openLobbiesPageIfFailure) {
|
||||
openLobbiesWindow(runtimeScene);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const authenticateAndQuickJoinLobby = async (
|
||||
runtimeScene: gdjs.RuntimeScene,
|
||||
displayLoader: boolean,
|
||||
openLobbiesPageIfFailure: boolean
|
||||
) => {
|
||||
const requestDoneAt = Date.now();
|
||||
if (_lastQuickJoinRequestDoneAt) {
|
||||
if (requestDoneAt - _lastQuickJoinRequestDoneAt < 500) {
|
||||
_lastQuickJoinRequestDoneAt = requestDoneAt;
|
||||
logger.warn(
|
||||
'Last request to quick join a lobby was sent too little time ago. Ignoring this one.'
|
||||
);
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
_lastQuickJoinRequestDoneAt = requestDoneAt;
|
||||
}
|
||||
|
||||
const playerId = gdjs.playerAuthentication.getUserId();
|
||||
const playerToken = gdjs.playerAuthentication.getUserToken();
|
||||
if (!playerId || !playerToken) {
|
||||
_isWaitingForLogin = true;
|
||||
const {
|
||||
status,
|
||||
} = await gdjs.playerAuthentication.openAuthenticationWindow(
|
||||
runtimeScene
|
||||
).promise;
|
||||
_isWaitingForLogin = false;
|
||||
|
||||
if (status === 'logged') {
|
||||
await quickJoinLobby(
|
||||
runtimeScene,
|
||||
displayLoader,
|
||||
openLobbiesPageIfFailure
|
||||
);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
await quickJoinLobby(
|
||||
runtimeScene,
|
||||
displayLoader,
|
||||
openLobbiesPageIfFailure
|
||||
);
|
||||
};
|
||||
|
||||
export const isSearchingForLobbyToJoin = (
|
||||
runtimeScene: gdjs.RuntimeScene
|
||||
) => {
|
||||
return _isQuickJoiningOrStartingAGame;
|
||||
};
|
||||
|
||||
export const hasQuickJoinJustFailed = (runtimeScene: gdjs.RuntimeScene) => {
|
||||
return _quickJoinLobbyJustFailed;
|
||||
};
|
||||
|
||||
export const getQuickJoinFailureReason = () => {
|
||||
return _quickJoinLobbyFailureReason;
|
||||
};
|
||||
|
||||
/**
|
||||
* Action to display the lobbies window to the user.
|
||||
*/
|
||||
|
@@ -11,6 +11,10 @@ namespace gdjs {
|
||||
data: Uint8Array | string;
|
||||
};
|
||||
|
||||
type PeerJSInitOptions = {
|
||||
onPeerUnavailable?: () => void;
|
||||
};
|
||||
|
||||
export type CompressionMethod = 'none' | 'cs:gzip' | 'cs:deflate';
|
||||
|
||||
/**
|
||||
@@ -100,6 +104,8 @@ namespace gdjs {
|
||||
*/
|
||||
let ready = false;
|
||||
|
||||
let _peerIdToConnectToOnceReady: string | null = null;
|
||||
|
||||
/**
|
||||
* List of IDs of peers that just disconnected.
|
||||
*/
|
||||
@@ -228,6 +234,14 @@ namespace gdjs {
|
||||
return newMessagesList;
|
||||
};
|
||||
|
||||
const _onReady = () => {
|
||||
ready = true;
|
||||
if (_peerIdToConnectToOnceReady) {
|
||||
connect(_peerIdToConnectToOnceReady);
|
||||
_peerIdToConnectToOnceReady = null;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Internal function called when a connection with a remote peer is initiated.
|
||||
* @param connection The DataConnection of the peer
|
||||
@@ -289,14 +303,28 @@ namespace gdjs {
|
||||
* Internal function called to initialize PeerJS after it
|
||||
* has been configured.
|
||||
*/
|
||||
const loadPeerJS = () => {
|
||||
const initializePeerJS = (initOptions: PeerJSInitOptions = {}) => {
|
||||
if (peer !== null) return;
|
||||
peer = new Peer(peerConfig);
|
||||
peer.on('open', () => {
|
||||
ready = true;
|
||||
_onReady();
|
||||
});
|
||||
peer.on('error', (errorMessage) => {
|
||||
logger.error('PeerJS error:', errorMessage);
|
||||
peer.on('error', (error) => {
|
||||
// TODO: Support other error types listed in https://peerjs.com/docs/#peeron-error
|
||||
if (
|
||||
initOptions.onPeerUnavailable &&
|
||||
// @ts-ignore - PeerJS adds `type` on errors, but it doesn't show in their TS types.
|
||||
error.type === 'peer-unavailable'
|
||||
) {
|
||||
logger.error('Peer is unavailable.');
|
||||
initOptions.onPeerUnavailable();
|
||||
} else {
|
||||
logger.error(
|
||||
// @ts-ignore - PeerJS adds `type` on errors, but it doesn't show in their TS types.
|
||||
`PeerJS error (${error.type || 'unknown'}):`,
|
||||
error
|
||||
);
|
||||
}
|
||||
});
|
||||
peer.on('connection', (connection) => {
|
||||
connection.on('open', () => {
|
||||
@@ -306,17 +334,21 @@ namespace gdjs {
|
||||
});
|
||||
peer.on('close', () => {
|
||||
peer = null;
|
||||
loadPeerJS();
|
||||
initializePeerJS(initOptions);
|
||||
});
|
||||
peer.on('disconnected', peer.reconnect);
|
||||
};
|
||||
export const useDefaultBrokerServer = initializePeerJS;
|
||||
|
||||
/**
|
||||
* Connects to another p2p client.
|
||||
* @param id - The other client's ID.
|
||||
*/
|
||||
export const connect = (id: string) => {
|
||||
if (peer === null) return;
|
||||
if (peer === null || !ready) {
|
||||
_peerIdToConnectToOnceReady = id;
|
||||
return;
|
||||
}
|
||||
const connection = peer.connect(id);
|
||||
connection.on('open', () => {
|
||||
_onConnect(connection);
|
||||
@@ -365,13 +397,15 @@ namespace gdjs {
|
||||
* @param path The path (part of the url after the host) to the broker server.
|
||||
* @param key Optional password to connect to the broker server.
|
||||
* @param ssl Use ssl?
|
||||
* @param peerJSInitOptions @see PeerJSInitOptions
|
||||
*/
|
||||
export const useCustomBrokerServer = (
|
||||
host: string,
|
||||
port: number,
|
||||
path: string,
|
||||
key: string,
|
||||
ssl: boolean
|
||||
ssl: boolean,
|
||||
peerJSInitOptions: PeerJSInitOptions = {}
|
||||
) => {
|
||||
Object.assign(peerConfig, {
|
||||
host,
|
||||
@@ -381,11 +415,9 @@ namespace gdjs {
|
||||
// All servers have "peerjs" as default key
|
||||
key: key.length === 0 ? 'peerjs' : key,
|
||||
});
|
||||
loadPeerJS();
|
||||
initializePeerJS(peerJSInitOptions);
|
||||
};
|
||||
|
||||
export const useDefaultBrokerServer = loadPeerJS;
|
||||
|
||||
/**
|
||||
* Adds an ICE server candidate, and removes the default ones provided by PeerJs. Must be called before connecting to a broker.
|
||||
* @param urls The URL of the STUN/TURN server.
|
||||
@@ -422,8 +454,8 @@ namespace gdjs {
|
||||
* @see Peer.id
|
||||
*/
|
||||
export const getCurrentId = (): string => {
|
||||
if (peer == undefined) return '';
|
||||
return peer.id || '';
|
||||
if (peer === null) return '';
|
||||
return peer.id;
|
||||
};
|
||||
|
||||
/**
|
||||
|
@@ -82,7 +82,7 @@ namespace gdjs {
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (this._ws.readyState === 1) this._ws.send(message);
|
||||
if (this._ws.readyState === WebSocket.OPEN) this._ws.send(message);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -45,8 +45,7 @@ namespace gdjs {
|
||||
request.ontimeout = onError;
|
||||
request.onabort = onError;
|
||||
request.onreadystatechange = () => {
|
||||
/* "DONE" */
|
||||
if (request.readyState === 4) {
|
||||
if (request.readyState === XMLHttpRequest.DONE) {
|
||||
if (request.status >= 400) {
|
||||
onError('' + request.status);
|
||||
}
|
||||
@@ -97,6 +96,29 @@ namespace gdjs {
|
||||
);
|
||||
};
|
||||
|
||||
const delay = (ms: number): Promise<void> =>
|
||||
new Promise((res) => setTimeout(res, ms));
|
||||
|
||||
export const retryIfFailed = async <T>(
|
||||
{ times, delayInMs }: { times: number; delayInMs?: number },
|
||||
functionCalled: () => Promise<T>
|
||||
): Promise<T> => {
|
||||
let tries = 0;
|
||||
let latestError = null;
|
||||
while (tries < times) {
|
||||
tries++;
|
||||
latestError = null;
|
||||
try {
|
||||
const latestReturnValue = await functionCalled();
|
||||
return latestReturnValue;
|
||||
} catch (error) {
|
||||
if (delayInMs) await delay(delayInMs);
|
||||
latestError = error;
|
||||
}
|
||||
}
|
||||
|
||||
throw latestError;
|
||||
};
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
|
@@ -367,7 +367,7 @@ namespace gdjs {
|
||||
/**
|
||||
* Register a function to be called when a scene unload started. This is
|
||||
* before the object deletion and renderer destruction. It is safe to
|
||||
* manipulate these. It is **not** be safe to release resources as other
|
||||
* manipulate these. It is **not** safe to release resources as other
|
||||
* callbacks might do operations on objects or the scene.
|
||||
*
|
||||
* @param callback The function to be called.
|
||||
|
Reference in New Issue
Block a user