From c40b8b5d8eb9aaa5bb96e12776bac9a722ab2ace Mon Sep 17 00:00:00 2001 From: Liam Date: Sat, 19 Oct 2024 04:53:06 +0100 Subject: [PATCH] migrate around me to the backend --- .../src/controller/player.controller.ts | 18 ++++ projects/backend/src/index.ts | 2 +- .../backend/src/service/player.service.ts | 70 +++++++++++++- projects/backend/src/service/score.service.ts | 2 - .../src/response/around-player-response.ts | 8 ++ projects/common/src/types/around-player.ts | 1 + projects/common/src/utils/player-utils.ts | 12 +++ projects/common/src/utils/utils.ts | 4 +- .../src/websocket/scoresaber-websocket.ts | 6 +- .../website/src/components/ranking/mini.tsx | 92 +++---------------- 10 files changed, 123 insertions(+), 92 deletions(-) create mode 100644 projects/common/src/response/around-player-response.ts create mode 100644 projects/common/src/types/around-player.ts diff --git a/projects/backend/src/controller/player.controller.ts b/projects/backend/src/controller/player.controller.ts index e72ef67..208d467 100644 --- a/projects/backend/src/controller/player.controller.ts +++ b/projects/backend/src/controller/player.controller.ts @@ -3,6 +3,7 @@ import { PlayerService } from "../service/player.service"; import { t } from "elysia"; import { PlayerHistory } from "@ssr/common/player/player-history"; import { PlayerTrackedSince } from "@ssr/common/player/player-tracked-since"; +import { AroundPlayerResponse } from "@ssr/common/response/around-player-response"; @Controller("/player") export default class PlayerController { @@ -51,4 +52,21 @@ export default class PlayerController { }; } } + + @Get("/around/:id/:type", { + config: {}, + params: t.Object({ + id: t.String({ required: true }), + type: t.String({ required: true }), + }), + }) + public async getPlayersAround({ + params: { id, type }, + }: { + params: { id: string; type: "global" | "country" }; + }): Promise { + return { + players: await PlayerService.getAround(id, type), + }; + } } diff --git a/projects/backend/src/index.ts b/projects/backend/src/index.ts index c5df137..305f790 100644 --- a/projects/backend/src/index.ts +++ b/projects/backend/src/index.ts @@ -43,7 +43,7 @@ connectScoreSaberWebSocket({ onDisconnect: async error => { await logToChannel( DiscordChannels.backendLogs, - new EmbedBuilder().setDescription(`ScoreSaber websocket disconnected: ${error}`) + new EmbedBuilder().setDescription(`ScoreSaber websocket disconnected: ${error?.message}`) ); }, }); diff --git a/projects/backend/src/service/player.service.ts b/projects/backend/src/service/player.service.ts index 7d858c8..2f3d2a5 100644 --- a/projects/backend/src/service/player.service.ts +++ b/projects/backend/src/service/player.service.ts @@ -5,12 +5,11 @@ import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; import { InternalServerError } from "../error/internal-server-error"; import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore import { formatPp } from "@ssr/common/utils/number-utils"; -import { isProduction } from "@ssr/common/utils/utils"; +import { getPageFromRank, isProduction } from "@ssr/common/utils/utils"; import { DiscordChannels, logToChannel } from "../bot/bot"; import { EmbedBuilder } from "discord.js"; +import { AroundPlayer } from "@ssr/common/types/around-player"; export class PlayerService { /** @@ -197,4 +196,69 @@ export class PlayerService { `Updated scores set statistic for "${playerName}"(${playerId}), scores today: ${scores.rankedScores} ranked, ${scores.unrankedScores} unranked` ); } + + /** + * Gets the players around a player. + * + * @param id the player to get around + * @param type the type to get around + */ + public static async getAround(id: string, type: AroundPlayer): Promise { + const getRank = (player: ScoreSaberPlayerToken, type: AroundPlayer) => { + switch (type) { + case "global": + return player.rank; + case "country": + return player.countryRank; + } + }; + + const itemsPerPage = 50; + const player = await scoresaberService.lookupPlayer(id, true); + if (player == undefined) { + throw new NotFoundError(`Player "${id}" not found`); + } + const rank = getRank(player, type); + const rankWithinPage = rank % itemsPerPage; + + const pagesToSearch = [getPageFromRank(rank, itemsPerPage)]; + if (rankWithinPage > 0) { + pagesToSearch.push(getPageFromRank(rank - 1, itemsPerPage)); + } else if (rankWithinPage < itemsPerPage - 1) { + pagesToSearch.push(getPageFromRank(rank + 1, itemsPerPage)); + } + + const rankings: Map = new Map(); + for (const page of pagesToSearch) { + const response = + type == "global" + ? await scoresaberService.lookupPlayers(page) + : await scoresaberService.lookupPlayersByCountry(page, player.country); + if (response == undefined) { + continue; + } + + for (const player of response.players) { + if (rankings.has(player.id)) { + continue; + } + + rankings.set(player.id, player); + } + } + + const players = rankings + .values() + .toArray() + .sort((a, b) => { + return getRank(a, type) - getRank(b, type); + }); + + // Show 3 players above and 1 below the requested player + const playerPosition = players.findIndex(p => p.id === player.id); + const start = Math.max(0, playerPosition - 3); + const end = Math.min(players.length, playerPosition + 2); + + return players.slice(start, end); + } } diff --git a/projects/backend/src/service/score.service.ts b/projects/backend/src/service/score.service.ts index e5d60e8..3bf8e58 100644 --- a/projects/backend/src/service/score.service.ts +++ b/projects/backend/src/service/score.service.ts @@ -1,6 +1,4 @@ import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token"; -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils"; import { isProduction } from "@ssr/common/utils/utils"; import { Metadata } from "@ssr/common/types/metadata"; diff --git a/projects/common/src/response/around-player-response.ts b/projects/common/src/response/around-player-response.ts new file mode 100644 index 0000000..086cfe2 --- /dev/null +++ b/projects/common/src/response/around-player-response.ts @@ -0,0 +1,8 @@ +import ScoreSaberPlayerToken from "../types/token/scoresaber/score-saber-player-token"; + +export type AroundPlayerResponse = { + /** + * The players around the player. + */ + players: ScoreSaberPlayerToken[]; +}; diff --git a/projects/common/src/types/around-player.ts b/projects/common/src/types/around-player.ts new file mode 100644 index 0000000..b3adf25 --- /dev/null +++ b/projects/common/src/types/around-player.ts @@ -0,0 +1 @@ +export type AroundPlayer = "global" | "country"; diff --git a/projects/common/src/utils/player-utils.ts b/projects/common/src/utils/player-utils.ts index 9b263f3..3301adf 100644 --- a/projects/common/src/utils/player-utils.ts +++ b/projects/common/src/utils/player-utils.ts @@ -1,6 +1,8 @@ import { PlayerHistory } from "../player/player-history"; import { kyFetch } from "./utils"; import { Config } from "../config"; +import { AroundPlayer } from "../types/around-player"; +import { AroundPlayerResponse } from "../response/around-player-response"; /** * Sorts the player history based on date, @@ -22,3 +24,13 @@ export function sortPlayerHistory(history: Map) { export async function trackPlayer(id: string) { await kyFetch(`${Config.apiUrl}/player/history/${id}?createIfMissing=true`); } + +/** + * Gets the players around a player. + * + * @param id the player to get around + * @param type the type to get + */ +export async function getPlayersAroundPlayer(id: string, type: AroundPlayer) { + return await kyFetch(`${Config.apiUrl}/player/around/${id}/${type}`); +} diff --git a/projects/common/src/utils/utils.ts b/projects/common/src/utils/utils.ts index 66c8d46..fb8d6c8 100644 --- a/projects/common/src/utils/utils.ts +++ b/projects/common/src/utils/utils.ts @@ -28,10 +28,10 @@ export function delay(ms: number) { * * @param rank the rank * @param itemsPerPage the items per page - * @returns the page + * @returns the page number */ export function getPageFromRank(rank: number, itemsPerPage: number) { - return Math.floor(rank / itemsPerPage) + 1; + return Math.floor((rank - 1) / itemsPerPage) + 1; } /** diff --git a/projects/common/src/websocket/scoresaber-websocket.ts b/projects/common/src/websocket/scoresaber-websocket.ts index 1193a90..636ead4 100644 --- a/projects/common/src/websocket/scoresaber-websocket.ts +++ b/projects/common/src/websocket/scoresaber-websocket.ts @@ -21,7 +21,7 @@ type ScoresaberSocket = { * * @param error the error that caused the connection to close */ - onDisconnect?: (error?: WebSocket.ErrorEvent) => void; + onDisconnect?: (error?: WebSocket.ErrorEvent | WebSocket.CloseEvent) => void; }; /** @@ -46,10 +46,10 @@ export function connectScoreSaberWebSocket({ onMessage, onScore, onDisconnect }: onDisconnect && onDisconnect(error); }; - websocket.onclose = () => { + websocket.onclose = event => { console.log("Lost connection to the ScoreSaber WebSocket. Attempting to reconnect..."); - onDisconnect && onDisconnect(); + onDisconnect && onDisconnect(event); setTimeout(connectWs, 5000); // Reconnect after 5 seconds }; diff --git a/projects/website/src/components/ranking/mini.tsx b/projects/website/src/components/ranking/mini.tsx index f5a2a68..90a7af2 100644 --- a/projects/website/src/components/ranking/mini.tsx +++ b/projects/website/src/components/ranking/mini.tsx @@ -8,10 +8,8 @@ import CountryFlag from "../country-flag"; import { Avatar, AvatarImage } from "../ui/avatar"; import { PlayerRankingSkeleton } from "@/components/ranking/player-ranking-skeleton"; import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player"; -import { ScoreSaberPlayersPageToken } from "@ssr/common/types/token/scoresaber/score-saber-players-page-token"; -import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; -import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; -import { getPageFromRank } from "@ssr/common/utils/utils"; +import { getPlayersAroundPlayer } from "@ssr/common/utils/player-utils"; +import { AroundPlayer } from "@ssr/common/types/around-player"; const PLAYER_NAME_MAX_LENGTH = 18; @@ -34,42 +32,18 @@ type MiniProps = { type Variants = { [key: string]: { - itemsPerPage: number; icon: (player: ScoreSaberPlayer) => ReactElement; - getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => number; - getRank: (player: ScoreSaberPlayer) => number; - query: (page: number, country: string) => Promise; }; }; const miniVariants: Variants = { Global: { - itemsPerPage: 50, icon: () => , - getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => { - return getPageFromRank(player.rank - 1, itemsPerPage); - }, - getRank: (player: ScoreSaberPlayer) => { - return player.rank; - }, - query: (page: number) => { - return scoresaberService.lookupPlayers(page); - }, }, Country: { - itemsPerPage: 50, icon: (player: ScoreSaberPlayer) => { return ; }, - getPage: (player: ScoreSaberPlayer, itemsPerPage: number) => { - return getPageFromRank(player.countryRank - 1, itemsPerPage); - }, - getRank: (player: ScoreSaberPlayer) => { - return player.countryRank; - }, - query: (page: number, country: string) => { - return scoresaberService.lookupPlayersByCountry(page, country); - }, }, }; @@ -80,41 +54,18 @@ export default function Mini({ type, player, shouldUpdate }: MiniProps) { const variant = miniVariants[type]; const icon = variant.icon(player); - const itemsPerPage = variant.itemsPerPage; - // Calculate the page and the rank of the player within that page - const page = variant.getPage(player, itemsPerPage); - const rankWithinPage = variant.getRank(player) % itemsPerPage; - - const { data, isLoading, isError } = useQuery({ - queryKey: ["mini-ranking-" + type, player.id, type, page], - queryFn: async () => { - const pagesToSearch = [page]; - if (rankWithinPage < 5 && page > 1) { - pagesToSearch.push(page - 1); - } - if (rankWithinPage > itemsPerPage - 5) { - pagesToSearch.push(page + 1); - } - - const players: ScoreSaberPlayerToken[] = []; - for (const p of pagesToSearch) { - const response = await variant.query(p, player.country); - if (response) { - players.push(...response.players); - } - } - - // Sort players by rank to ensure correct order - return players.sort((a, b) => { - // This is the wrong type but it still works. - return variant.getRank(a as unknown as ScoreSaberPlayer) - variant.getRank(b as unknown as ScoreSaberPlayer); - }); - }, + const { + data: response, + isLoading, + isError, + } = useQuery({ + queryKey: ["mini-ranking-" + type, player.id, type], + queryFn: () => getPlayersAroundPlayer(player.id, type.toLowerCase() as AroundPlayer), enabled: shouldUpdate, }); - if (isLoading || !data) { + if (isLoading || !response) { return ; } @@ -122,27 +73,6 @@ export default function Mini({ type, player, shouldUpdate }: MiniProps) { return

Error loading ranking

; } - let players = data; - const playerPosition = players.findIndex(p => p.id === player.id); - - // Always show 5 players: 3 above and 1 below the player - const start = Math.max(0, playerPosition - 3); - const end = Math.min(players.length, start + 5); - - players = players.slice(start, end); - - // If fewer than 5 players, append/prepend more - if (players.length < 5) { - const missingPlayers = 5 - players.length; - if (start === 0) { - const additionalPlayers = players.slice(playerPosition + 1, playerPosition + 1 + missingPlayers); - players = [...players, ...additionalPlayers]; - } else if (end === players.length) { - const additionalPlayers = players.slice(Math.max(0, start - missingPlayers), start); - players = [...additionalPlayers, ...players]; - } - } - return (
@@ -150,7 +80,7 @@ export default function Mini({ type, player, shouldUpdate }: MiniProps) {

{type} Ranking

- {players.map((playerRanking, index) => { + {response.players.map((playerRanking, index) => { const rank = type == "Global" ? playerRanking.rank : playerRanking.countryRank; const playerName = playerRanking.name.length > PLAYER_NAME_MAX_LENGTH