Last Updated on April 5, 2025 by chase
This is the GetAroundPlayer function within an ILeaderboard module. The purpose of this function is to obtain leaderboard ranking information within a range, centered on any particular character id. It utilizes MongoDB to collect and evaluate the backend leaderboard data through aggregate queries in the leaderboard collection.
This is how the collectionSettings are setup to allow for efficient indexing on the following queries:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
const collectionSettings = { name: 'leaderboard', indexes: [ { field: 'playerId' }, { field: 'characterId' }, { field: { [`stats.${rankedStatId}.value`]: -1, [`stats.${rankedStatId}.lastUpdated`]: -1, }, }, ], options: {}, }; |
To obtain the characterRank of a character for a particular leaderboard statistic, this is the process:
{ $match: query }- First we validate that the requested statistic exists.
{ $sort: { [stats.${stat}.value]: -1, [stats.${stat}.lastUpdated]: -1 } }- Next we sort descendingly on two axis, first by the statistic value and then by its lastUpdated time.
- The lastUpdated time takes into account who has participated most recently.
- Next we sort descendingly on two axis, first by the statistic value and then by its lastUpdated time.
{ $group: { _id: null, characters: { $push: "$characterId" } } }- Using the results from our first two queries, we transform the results into a document that contains an array of character ids that is ordered based on our sort query.
{ "_id": null, "characters": [ "cid0", "cid1", ..., "cidN-1"] }
- Using the results from our first two queries, we transform the results into a document that contains an array of character ids that is ordered based on our sort query.
{ $unwind: { path: "$characters", includeArrayIndex: "rank" } }- We then unwind our group query based on the characters array, transforming the document to give us precise rank index information.
[{ "characters": "cid0", "rank": 0 }, ... { "characters": "cidN-1", "rank": N-1 }]
- We then unwind our group query based on the characters array, transforming the document to give us precise rank index information.
{ $match: { characters: cid } }- Now we just want the document corresponding to the passed in
cidparameter.
- Now we just want the document corresponding to the passed in
{ $project: { rank: 1 } }- From the match query, we just want information about the characters rank. Effectively giving us the index of the sorted rank index.
{ "rank": N }
- From the match query, we just want information about the characters rank. Effectively giving us the index of the sorted rank index.
{ $limit: 1 }- Finally, we limit the results to one as a safeguard, and align with our expectations.
After we have evaluated the characterRank, the next step is to retrieve up to the specified range parameter ILeaderboardEntry[] centered around this character id. Ideally, it would have been great to be able to reuse the first two aggregate steps, however at the time of writing MongoDB doesn’t allow this. We can however eliminate the $match query, as we know this has already been evaluated successfully at this point.
{ $sort: { [stats.${stat}.value]: -1, [stats.${stat}.lastUpdated]: -1 } }- First sort descendingly on two axis, first by the statistic value and then by its lastUpdated time.
- The lastUpdated time takes into account who has participated most recently.
- First sort descendingly on two axis, first by the statistic value and then by its lastUpdated time.
{ $skip: start }- Next we skip the results up to our evaluated document start index from the sorted results, omitting entries before the specified starting point.
{ $limit: end - start }- Finally we limit and return the entries within our evaluated range, ensuring the response matches the intended leaderboard segment.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
export const GetAroundPlayerFactory = (mongo: IMongoModule, members: IMemberModule): ILeaderboardGetAroundPlayerFunc => { return async (cid: string, stat: string, range: number): Promise<ILeaderboardEntry[]> =>{ const query = { [`stats.${stat}`]: { $exists: true }, }; const characterRank = await mongo.collection.aggregate([ { $match: query }, { $sort: { [`stats.${stat}.value`]: -1, [`stats.${stat}.lastUpdated`]: -1 } }, { $group: { _id: null, characters: { $push: "$characterId" } } }, { $unwind: { path: "$characters", includeArrayIndex: "rank" } }, { $match: { characters: cid } }, { $project: { rank: 1 } }, { $limit: 1 } ]); const characterRankResult = await characterRank.toArray(); if (characterRankResult.length === 0) { return []; } const rank = characterRankResult[0].rank; const halfRange = Math.floor(range / 2); const start = Math.max(0, rank - halfRange); const end = rank + halfRange; const cursor = await mongo.collection.aggregate([ { $sort: { [`stats.${stat}.value`]: -1, [`stats.${stat}.lastUpdated`]: -1 } }, { $skip: start }, { $limit: end - start } ]); const result = await cursor.toArray() as ILeaderboardEntry[]; if (result.length === 0) { return []; } const promises = _.map(result, async (entry, i) => { return members.getByLink(Platforms.Playfab, entry.playerId).then((member) => { return { playerId: entry.playerId, characterId: entry.characterId, stats: { [stat]: entry.stats[stat] }, device: member.contextData[ContextDataType.LastSeenDevice] as Devices, position: i + start } }) }); return Promise.all(promises); } } |
