Skip to content

Commit

Permalink
Allow player authentication in a leaderboard (#6552)
Browse files Browse the repository at this point in the history
* Enable leaderboards to launch authentication and claim an anonymous score that was just sent
* Also automatically attach a score sent to the logged in player (unless deactivated)
* Make various fixes for authentication on iOS
  • Loading branch information
4ian authored May 10, 2024
1 parent 701b5a3 commit 4ca859c
Show file tree
Hide file tree
Showing 15 changed files with 852 additions and 530 deletions.
43 changes: 42 additions & 1 deletion Extensions/Leaderboards/JsExtension.js
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ module.exports = {
.addAction(
'SavePlayerScore',
_('Save player score'),
_("Save the player's score to the given leaderboard."),
_(
"Save the player's score to the given leaderboard. If the player is connected, the score will be attached to the connected player (unless disabled)."
),
_(
'Send to leaderboard _PARAM1_ the score _PARAM2_ with player name: _PARAM3_'
),
Expand All @@ -60,6 +62,12 @@ module.exports = {
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/sha256.js')
.addIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.setFunctionName('gdjs.evtTools.leaderboards.savePlayerScore')
.setAsyncFunctionName('gdjs.evtTools.leaderboards.savePlayerScore');

Expand Down Expand Up @@ -87,11 +95,38 @@ module.exports = {
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/sha256.js')
.addIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.setFunctionName('gdjs.evtTools.leaderboards.saveConnectedPlayerScore')
.setAsyncFunctionName(
'gdjs.evtTools.leaderboards.saveConnectedPlayerScore'
);

extension
.addAction(
'SetPreferSendConnectedPlayerScore',
_('Always attach scores to the connected player'),
_(
'Set if the score sent to a leaderboard is always attached to the connected player - if any. This is on by default.'
),
_('Always attach the score to the connected player: _PARAM1_'),
_('Setup'),
'JsPlatform/Extensions/leaderboard.svg',
'JsPlatform/Extensions/leaderboard.svg'
)
.addCodeOnlyParameter('currentScene', '')
.addParameter('yesorno', _('Enable?'), '', false)
.setHelpPath('/all-features/leaderboards')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.setFunctionName(
'gdjs.evtTools.leaderboards.setPreferSendConnectedPlayerScore'
);

extension
.addCondition(
'HasLastSaveErrored',
Expand Down Expand Up @@ -273,6 +308,12 @@ module.exports = {
.setHelpPath('/all-features/leaderboards')
.getCodeExtraInformation()
.setIncludeFile('Extensions/Leaderboards/leaderboardstools.js')
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationcomponents.js'
)
.addIncludeFile(
'Extensions/PlayerAuthentication/playerauthenticationtools.js'
)
.setFunctionName('gdjs.evtTools.leaderboards.displayLeaderboard');

extension
Expand Down
150 changes: 124 additions & 26 deletions Extensions/Leaderboards/leaderboardstools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ namespace gdjs {
export namespace evtTools {
export namespace leaderboards {
let _hasPlayerJustClosedLeaderboardView = false;
let _preferSendConnectedPlayerScore = true;

gdjs.registerRuntimeScenePostEventsCallback(() => {
// Set it back to false for the next frame.
Expand All @@ -24,6 +25,14 @@ namespace gdjs {
return shaObj.getHash('B64');
};

const leaderboardHostBaseUrl = 'https://gd.games';
// const leaderboardHostBaseUrl = 'http://localhost:4000';

type PublicLeaderboardEntry = {
id: string;
claimSecret?: string;
};

/**
* Hold the state of the save of a score for a leaderboard.
*/
Expand All @@ -45,7 +54,7 @@ namespace gdjs {
private _lastSavedPlayerId: string | null = null;

/** The id of the entry in the leaderboard, for the last score saved with success. */
lastSavedLeaderboardEntryId: string | null = null;
lastSavedLeaderboardEntry: PublicLeaderboardEntry | null = null;

/** Last error that happened when saving the score (useful if `hasScoreSavingErrored` is true). */
lastSaveError: string | null = null;
Expand Down Expand Up @@ -114,7 +123,7 @@ namespace gdjs {
playerId?: string;
score: number;
}): {
closeSaving: (leaderboardEntryId: string | null) => void;
closeSaving: (leaderboardEntry: PublicLeaderboardEntry) => void;
closeSavingWithError(errorCode: string);
} {
if (this._isAlreadySavingThisScore({ playerName, playerId, score })) {
Expand Down Expand Up @@ -159,7 +168,7 @@ namespace gdjs {
if (playerId) this._currentlySavingPlayerId = playerId;

return {
closeSaving: (leaderboardEntryId) => {
closeSaving: (leaderboardEntry) => {
if (savingPromise !== this.lastSavingPromise) {
logger.info(
'Score saving result received, but another save was launched in the meantime - ignoring the result of this one.'
Expand All @@ -174,7 +183,7 @@ namespace gdjs {
this._lastSavedScore = this._currentlySavingScore;
this._lastSavedPlayerName = this._currentlySavingPlayerName;
this._lastSavedPlayerId = this._currentlySavingPlayerId;
this.lastSavedLeaderboardEntryId = leaderboardEntryId;
this.lastSavedLeaderboardEntry = leaderboardEntry;
this.hasScoreBeenSaved = true;

resolveSavingPromise();
Expand Down Expand Up @@ -214,7 +223,7 @@ namespace gdjs {
let _leaderboardViewIframeLoading: boolean = false;
let _leaderboardViewIframeLoaded: boolean = false;
let _errorTimeoutId: NodeJS.Timeout | null = null;
let _leaderboardViewClosingCallback:
let _leaderboardMessageListener:
| ((event: MessageEvent) => void)
| null = null;

Expand Down Expand Up @@ -288,7 +297,7 @@ namespace gdjs {
authenticatedPlayerData?: { playerId: string; playerToken: string };
score: number;
runtimeScene: gdjs.RuntimeScene;
}) {
}): Promise<PublicLeaderboardEntry> {
const rootApi = runtimeScene
.getGame()
.isUsingGDevelopDevelopmentEnvironment()
Expand Down Expand Up @@ -339,18 +348,18 @@ namespace gdjs {
throw errorCode;
}

let leaderboardEntryId: string | null = null;
try {
const leaderboardEntry = await response.json();
leaderboardEntryId = leaderboardEntry.id;
return leaderboardEntry;
} catch (error) {
logger.warn(
'An error occurred when reading response but score has been saved:',
error
);
}

return leaderboardEntryId;
const errorCode = 'SAVED_ENTRY_CANT_BE_READ';
throw errorCode;
}
} catch (error) {
logger.error('Error while submitting a leaderboard score:', error);
const errorCode = 'REQUEST_NOT_SENT';
Expand All @@ -359,13 +368,27 @@ namespace gdjs {
}
};

export const setPreferSendConnectedPlayerScore = (
runtimeScene: gdjs.RuntimeScene,
enable: boolean
) => {
_preferSendConnectedPlayerScore = enable;
};

export const savePlayerScore = (
runtimeScene: gdjs.RuntimeScene,
leaderboardId: string,
score: float,
playerName: string
) =>
new gdjs.PromiseTask(
) => {
if (
_preferSendConnectedPlayerScore &&
gdjs.playerAuthentication.isAuthenticated()
) {
return saveConnectedPlayerScore(runtimeScene, leaderboardId, score);
}

return new gdjs.PromiseTask(
(async () => {
const scoreSavingState = (_scoreSavingStateByLeaderboard[
leaderboardId
Expand All @@ -380,13 +403,13 @@ namespace gdjs {
} = scoreSavingState.startSaving({ playerName, score });

try {
const leaderboardEntryId = await saveScore({
const leaderboardEntry = await saveScore({
leaderboardId,
playerName,
score,
runtimeScene,
});
closeSaving(leaderboardEntryId);
closeSaving(leaderboardEntry);
} catch (errorCode) {
closeSavingWithError(errorCode);
}
Expand All @@ -395,6 +418,7 @@ namespace gdjs {
}
})()
);
};

export const saveConnectedPlayerScore = (
runtimeScene: gdjs.RuntimeScene,
Expand Down Expand Up @@ -551,7 +575,64 @@ namespace gdjs {
displayLoader: boolean,
event: MessageEvent
) {
switch (event.data) {
const messageId =
typeof event.data === 'string' ? event.data : event.data.id;
switch (messageId) {
case 'playerAuthenticated':
gdjs.playerAuthentication.login({
runtimeScene,
userId: event.data.userId,
username: event.data.username,
userToken: event.data.userToken,
});
break;
case 'openPlayerAuthentication':
gdjs.playerAuthentication
.openAuthenticationWindow(runtimeScene)
.promise.then(({ status }) => {
if (
!_leaderboardViewIframe ||
!_leaderboardViewIframe.contentWindow
) {
logger.warn(
'Unable to transmit the new login status to the leaderboard view.'
);
return;
}

if (status === 'errored') {
_leaderboardViewIframe.contentWindow.postMessage(
{
id: 'onPlayerAuthenticationErrored',
},
leaderboardHostBaseUrl
);
return;
}

const playerId = gdjs.playerAuthentication.getUserId();
const playerToken = gdjs.playerAuthentication.getUserToken();
if (status === 'dismissed' || !playerId || !playerToken) {
_leaderboardViewIframe.contentWindow.postMessage(
{
id: 'onPlayerAuthenticationDismissed',
},
leaderboardHostBaseUrl
);
return;
}

_leaderboardViewIframe.contentWindow.postMessage(
{
id: 'onPlayerAuthenticated',
playerId,
playerUsername: gdjs.playerAuthentication.getUsername(),
playerToken: playerToken,
},
leaderboardHostBaseUrl
);
});
break;
case 'closeLeaderboardView':
_hasPlayerJustClosedLeaderboardView = true;
closeLeaderboardView(runtimeScene);
Expand Down Expand Up @@ -599,7 +680,7 @@ namespace gdjs {
'Leaderboard page did not send message in time. Closing leaderboard view.'
);
}
}, 5000);
}, 15000);
};

const displayLoaderInLeaderboardView = function (
Expand Down Expand Up @@ -701,15 +782,15 @@ namespace gdjs {
});
}

// If a save is being done for this leaderboard, wait for it to end so that the `lastSavedLeaderboardEntryId`
// If a save is being done for this leaderboard, wait for it to end so that the `lastSavedLeaderboardEntry`
// can be saved and then used to show the player score.
const scoreSavingState = _scoreSavingStateByLeaderboard[leaderboardId];
if (scoreSavingState && scoreSavingState.lastSavingPromise) {
await scoreSavingState.lastSavingPromise;
}

const lastSavedLeaderboardEntryId = scoreSavingState
? scoreSavingState.lastSavedLeaderboardEntryId
const lastSavedLeaderboardEntry = scoreSavingState
? scoreSavingState.lastSavedLeaderboardEntry
: null;

const gameId = gdjs.projectData.properties.projectUuid;
Expand All @@ -720,13 +801,30 @@ namespace gdjs {
const searchParams = new URLSearchParams();
searchParams.set('inGameEmbedded', 'true');
if (isDev) searchParams.set('dev', 'true');
if (lastSavedLeaderboardEntryId)
if (lastSavedLeaderboardEntry) {
searchParams.set(
'playerLeaderboardEntryId',
lastSavedLeaderboardEntryId
lastSavedLeaderboardEntry.id
);
if (lastSavedLeaderboardEntry.claimSecret) {
searchParams.set(
'playerLeaderboardEntryClaimSecret',
lastSavedLeaderboardEntry.claimSecret
);
}
}
const playerId = gdjs.playerAuthentication.getUserId();
const playerToken = gdjs.playerAuthentication.getUserToken();
if (playerId && playerToken) {
searchParams.set('playerId', playerId);
searchParams.set('playerToken', playerToken);
searchParams.set(
'playerUsername',
gdjs.playerAuthentication.getUsername()
);
}

const targetUrl = `https://gd.games/games/${gameId}/leaderboard/${leaderboardId}?${searchParams}`;
const targetUrl = `${leaderboardHostBaseUrl}/games/${gameId}/leaderboard/${leaderboardId}?${searchParams}`;

try {
const isAvailable = await checkLeaderboardAvailability(targetUrl);
Expand Down Expand Up @@ -772,7 +870,7 @@ namespace gdjs {
targetUrl
);
if (typeof window !== 'undefined') {
_leaderboardViewClosingCallback = (event: MessageEvent) => {
_leaderboardMessageListener = (event: MessageEvent) => {
receiveMessageFromLeaderboardView(
runtimeScene,
displayLoader,
Expand All @@ -781,7 +879,7 @@ namespace gdjs {
};
(window as any).addEventListener(
'message',
_leaderboardViewClosingCallback,
_leaderboardMessageListener,
true
);
}
Expand Down Expand Up @@ -836,10 +934,10 @@ namespace gdjs {
if (typeof window !== 'undefined') {
(window as any).removeEventListener(
'message',
_leaderboardViewClosingCallback,
_leaderboardMessageListener,
true
);
_leaderboardViewClosingCallback = null;
_leaderboardMessageListener = null;
}
domElementContainer.removeChild(_leaderboardViewIframe);
_leaderboardViewIframe = null;
Expand Down
Loading

0 comments on commit 4ca859c

Please sign in to comment.