diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 59ea0f2..3bd82d7 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,3 +1,4 @@ +import { AppProvider } from "@/components/AppProvider"; import { ssrSettings } from "@/ssrSettings"; import { Metadata } from "next"; import { Inter } from "next/font/google"; @@ -40,7 +41,7 @@ export default function RootLayout({ /> - {children} + {children} ); diff --git a/src/app/player/[id]/page.tsx b/src/app/player/[id]/page.tsx index 59410c9..dcb78cd 100644 --- a/src/app/player/[id]/page.tsx +++ b/src/app/player/[id]/page.tsx @@ -9,7 +9,7 @@ import Scores from "@/components/player/Scores"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { useSettingsStore } from "@/store/settingsStore"; import { SortType, SortTypes } from "@/types/SortTypes"; -import { getPlayerInfo } from "@/utils/scoresaber/api"; +import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import { useSearchParams } from "next/navigation"; import { useEffect, useState } from "react"; @@ -59,7 +59,7 @@ export default function Player({ params }: { params: { id: string } }) { return; } - getPlayerInfo(params.id).then((playerResponse) => { + ScoreSaberAPI.getPlayerInfo(params.id).then((playerResponse) => { if (!playerResponse) { setError(true); setErrorMessage("Failed to fetch player. Is the ID correct?"); diff --git a/src/app/ranking/country/[country]/page.tsx b/src/app/ranking/country/[country]/page.tsx index 7e9014b..5d4e271 100644 --- a/src/app/ranking/country/[country]/page.tsx +++ b/src/app/ranking/country/[country]/page.tsx @@ -8,7 +8,7 @@ import { Spinner } from "@/components/Spinner"; import PlayerRanking from "@/components/player/PlayerRanking"; import PlayerRankingMobile from "@/components/player/PlayerRankingMobile"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; -import { fetchTopPlayers } from "@/utils/scoresaber/api"; +import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import { normalizedRegionName } from "@/utils/utils"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; @@ -53,7 +53,7 @@ export default function RankingCountry({ params }: RankingCountryProps) { const updatePage = useCallback( (page: any) => { console.log("Switching page to", page); - fetchTopPlayers(page, country).then((response) => { + ScoreSaberAPI.fetchTopPlayers(page, country).then((response) => { if (!response) { setError(true); setErrorMessage("No players found"); diff --git a/src/app/ranking/global/page.tsx b/src/app/ranking/global/page.tsx index 43a19d4..2a6666e 100644 --- a/src/app/ranking/global/page.tsx +++ b/src/app/ranking/global/page.tsx @@ -8,7 +8,7 @@ import { Spinner } from "@/components/Spinner"; import PlayerRanking from "@/components/player/PlayerRanking"; import PlayerRankingMobile from "@/components/player/PlayerRankingMobile"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; -import { fetchTopPlayers } from "@/utils/scoresaber/api"; +import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import { GlobeAsiaAustraliaIcon } from "@heroicons/react/20/solid"; import Link from "next/link"; import { useRouter, useSearchParams } from "next/navigation"; @@ -46,7 +46,7 @@ export default function RankingGlobal() { const updatePage = useCallback( (page: any) => { console.log("Switching page to", page); - fetchTopPlayers(page).then((response) => { + ScoreSaberAPI.fetchTopPlayers(page).then((response) => { if (!response) { setError(true); setErrorMessage("No players found"); diff --git a/src/components/AppProvider.tsx b/src/components/AppProvider.tsx new file mode 100644 index 0000000..4bc5fd2 --- /dev/null +++ b/src/components/AppProvider.tsx @@ -0,0 +1,24 @@ +"use client"; + +import { useBeatLeaderScoresStore } from "@/store/beatLeaderScoresStore"; +import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore"; + +type AppProviderProps = { + children: React.ReactNode; +}; + +export function AppProvider({ children }: AppProviderProps) { + return <>{children}; +} + +const UPDATE_INTERVAL = 1000 * 60 * 15; // 15 minutes + +useBeatLeaderScoresStore.getState().updatePlayerScores(); +setInterval(() => { + useBeatLeaderScoresStore.getState().updatePlayerScores(); +}, UPDATE_INTERVAL); + +useScoresaberScoresStore.getState().updatePlayerScores(); +setInterval(() => { + useScoresaberScoresStore.getState().updatePlayerScores(); +}, UPDATE_INTERVAL); diff --git a/src/components/player/PlayerInfo.tsx b/src/components/player/PlayerInfo.tsx index 97aaf08..cecd369 100644 --- a/src/components/player/PlayerInfo.tsx +++ b/src/components/player/PlayerInfo.tsx @@ -1,5 +1,6 @@ import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; -import { usePlayerScoresStore } from "@/store/playerScoresStore"; +import { useBeatLeaderScoresStore } from "@/store/beatLeaderScoresStore"; +import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore"; import { useSettingsStore } from "@/store/settingsStore"; import { formatNumber } from "@/utils/number"; import { @@ -28,7 +29,7 @@ type PlayerInfoProps = { export default function PlayerInfo({ playerData }: PlayerInfoProps) { const playerId = playerData.id; const settingsStore = useStore(useSettingsStore, (store) => store); - const playerScoreStore = useStore(usePlayerScoresStore, (store) => store); + const playerScoreStore = useStore(useScoresaberScoresStore, (store) => store); // Whether we have scores for this player in the local database const hasLocalScores = playerScoreStore?.exists(playerId); @@ -50,39 +51,80 @@ export default function PlayerInfo({ playerData }: PlayerInfoProps) { } async function addProfile(isFriend: boolean) { - if (!usePlayerScoresStore.getState().exists(playerId)) { - const reponse = await playerScoreStore?.addPlayer( - playerId, - (page, totalPages) => { - const autoClose = page == totalPages ? 5000 : false; + const setupScoresaber = async () => { + if (!useScoresaberScoresStore.getState().exists(playerId)) { + const reponse = await playerScoreStore?.addPlayer( + playerId, + (page, totalPages) => { + const autoClose = page == totalPages ? 5000 : false; - if (page == 1) { - toastId.current = toast.info( - `Fetching scores ${page}/${totalPages}`, - { - autoClose: autoClose, + if (page == 1) { + toastId.current = toast.info( + `Fetching ScoreSaber scores ${page}/${totalPages}`, + { + autoClose: autoClose, + progress: page / totalPages, + }, + ); + } else { + toast.update(toastId.current, { progress: page / totalPages, - }, - ); - } else { - toast.update(toastId.current, { - progress: page / totalPages, - render: `Fetching scores ${page}/${totalPages}`, - autoClose: autoClose, - }); - } + render: `Fetching ScoreSaber scores ${page}/${totalPages}`, + autoClose: autoClose, + }); + } - console.log( - `Fetching scores for ${playerId} (${page}/${totalPages})`, - ); - }, - ); - if (reponse?.error) { - toast.error("Failed to fetch scores"); - console.log(reponse.message); - return; + console.log( + `Fetching ScoreSaber scores for ${playerId} (${page}/${totalPages})`, + ); + }, + ); + if (reponse?.error) { + toast.error("Failed to fetch scores"); + console.log(reponse.message); + return; + } } - } + }; + + const setupBeatleader = async () => { + if (!useBeatLeaderScoresStore.getState().exists(playerId)) { + const reponse = await playerScoreStore?.addPlayer( + playerId, + (page, totalPages) => { + const autoClose = page == totalPages ? 5000 : false; + + if (page == 1) { + toastId.current = toast.info( + `Fetching BeatLeader scores ${page}/${totalPages}`, + { + autoClose: autoClose, + progress: page / totalPages, + }, + ); + } else { + toast.update(toastId.current, { + progress: page / totalPages, + render: `Fetching BeatLeader scores ${page}/${totalPages}`, + autoClose: autoClose, + }); + } + + console.log( + `Fetching BeatLeader scores for ${playerId} (${page}/${totalPages})`, + ); + }, + ); + if (reponse?.error) { + toast.error("Failed to fetch scores"); + console.log(reponse.message); + return; + } + } + }; + + await setupScoresaber(); + await setupBeatleader(); if (!isFriend) { toast.success(`Successfully set ${playerData.name} as your profile`); diff --git a/src/components/player/Score.tsx b/src/components/player/Score.tsx index 006cf2c..0b30c4f 100644 --- a/src/components/player/Score.tsx +++ b/src/components/player/Score.tsx @@ -1,7 +1,13 @@ import { ScoresaberLeaderboardInfo } from "@/schemas/scoresaber/leaderboard"; +import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { ScoresaberScore } from "@/schemas/scoresaber/score"; +import { useBeatLeaderScoresStore } from "@/store/beatLeaderScoresStore"; import { formatNumber } from "@/utils/number"; -import { GlobeAsiaAustraliaIcon } from "@heroicons/react/20/solid"; +import { + CheckIcon, + GlobeAsiaAustraliaIcon, + XMarkIcon, +} from "@heroicons/react/20/solid"; import clsx from "clsx"; import moment from "moment"; import Image from "next/image"; @@ -9,11 +15,17 @@ import ScoreStatLabel from "./ScoreStatLabel"; type ScoreProps = { score: ScoresaberScore; + player: ScoresaberPlayer; leaderboard: ScoresaberLeaderboardInfo; }; -export default function Score({ score, leaderboard }: ScoreProps) { +export default function Score({ score, player, leaderboard }: ScoreProps) { const isFullCombo = score.missedNotes + score.badCuts === 0; + const beatleaderScoreData = useBeatLeaderScoresStore + .getState() + .getScore(player.id, leaderboard.songHash); + + console.log(beatleaderScoreData); return (
@@ -90,7 +102,13 @@ export default function Score({ score, leaderboard }: ScoreProps) { "min-w-[2rem]", isFullCombo ? "bg-green-500" : "bg-red-500", )} - title={`${score.missedNotes} missed notes. ${score.badCuts} bad cuts.`} + icon={ + isFullCombo ? ( + + ) : ( + + ) + } value={ isFullCombo ? "FC" diff --git a/src/components/player/Scores.tsx b/src/components/player/Scores.tsx index 9bffeff..4077ad0 100644 --- a/src/components/player/Scores.tsx +++ b/src/components/player/Scores.tsx @@ -2,7 +2,7 @@ import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore"; import { useSettingsStore } from "@/store/settingsStore"; import { SortType, SortTypes } from "@/types/SortTypes"; -import { fetchScores } from "@/utils/scoresaber/api"; +import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import { useRouter } from "next/navigation"; import { useCallback, useEffect, useState } from "react"; import Card from "../Card"; @@ -43,38 +43,40 @@ export default function Scores({ playerData, page, sortType }: ScoresProps) { const updateScoresPage = useCallback( (sortType: SortType, page: any) => { console.log(`Switching page to ${page} with sort ${sortType.value}`); - fetchScores(playerId, page, sortType.value, 10).then((scoresResponse) => { - if (!scoresResponse) { - setError(true); - setErrorMessage("No Scores"); - setScores({ ...scores, loading: false }); - return; - } - setScores({ - ...scores, - scores: scoresResponse.scores, - totalPages: scoresResponse.pageInfo.totalPages, - loading: false, - page: page, - sortType: sortType, - }); - useSettingsStore.setState({ - lastUsedSortType: sortType, - }); - - if (page > 1) { - router.push( - `/player/${playerId}?page=${page}&sort=${sortType.value}`, - { - scroll: false, - }, - ); - } else { - router.push(`/player/${playerId}?sort=${sortType.value}`, { - scroll: false, + ScoreSaberAPI.fetchScores(playerId, page, sortType.value, 10).then( + (scoresResponse) => { + if (!scoresResponse) { + setError(true); + setErrorMessage("No Scores"); + setScores({ ...scores, loading: false }); + return; + } + setScores({ + ...scores, + scores: scoresResponse.scores, + totalPages: scoresResponse.pageInfo.totalPages, + loading: false, + page: page, + sortType: sortType, }); - } - }); + useSettingsStore.setState({ + lastUsedSortType: sortType, + }); + + if (page > 1) { + router.push( + `/player/${playerId}?page=${page}&sort=${sortType.value}`, + { + scroll: false, + }, + ); + } else { + router.push(`/player/${playerId}?sort=${sortType.value}`, { + scroll: false, + }); + } + }, + ); }, [playerId, router, scores], ); @@ -125,7 +127,12 @@ export default function Scores({ playerData, page, sortType }: ScoresProps) { const { score, leaderboard } = scoreData; return ( - + ); }) )} diff --git a/src/components/player/SearchPlayer.tsx b/src/components/player/SearchPlayer.tsx index 926b98c..133b8e9 100644 --- a/src/components/player/SearchPlayer.tsx +++ b/src/components/player/SearchPlayer.tsx @@ -2,7 +2,7 @@ import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { formatNumber } from "@/utils/number"; -import { getPlayerInfo, searchByName } from "@/utils/scoresaber/api"; +import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import { MagnifyingGlassIcon } from "@heroicons/react/20/solid"; import clsx from "clsx"; import { useEffect, useState } from "react"; @@ -27,14 +27,14 @@ export default function SearchPlayer() { const id = search.split("/").pop(); if (id == undefined) return; - const player = await getPlayerInfo(id); + const player = await ScoreSaberAPI.getPlayerInfo(id); if (player == undefined) return; setPlayers([player]); } // Search by name - const players = await searchByName(search); + const players = await ScoreSaberAPI.searchByName(search); if (players == undefined) return; setPlayers(players); diff --git a/src/schemas/beatleader/difficulty.ts b/src/schemas/beatleader/difficulty.ts new file mode 100644 index 0000000..f3d69b3 --- /dev/null +++ b/src/schemas/beatleader/difficulty.ts @@ -0,0 +1,30 @@ +import { BeatleaderModifierRating } from "./modifierRating"; +import { BeatleaderModifier } from "./modifiers"; + +export type BeatleaderDifficulty = { + id: number; + value: number; + mode: number; + difficultyName: string; + modeName: string; + status: number; + modifierValues: BeatleaderModifier; + modifiersRating: BeatleaderModifierRating; + nominatedTime: number; + qualifiedTime: number; + rankedTime: number; + stars: number; + predictedAcc: number; + passRating: number; + accRating: number; + techRating: number; + type: number; + njs: number; + nps: number; + notes: number; + bombs: number; + walls: number; + maxScore: number; + duration: number; + requirements: number; +}; diff --git a/src/schemas/beatleader/leaderboard.ts b/src/schemas/beatleader/leaderboard.ts new file mode 100644 index 0000000..42c8771 --- /dev/null +++ b/src/schemas/beatleader/leaderboard.ts @@ -0,0 +1,16 @@ +import { BeatleaderDifficulty } from "./difficulty"; +import { BeatleaderSong } from "./song"; + +export type BeatleaderLeaderboard = { + id: string; + song: BeatleaderSong; + difficulty: BeatleaderDifficulty; + scores: null; // ?? + changes: null; // ?? + qualification: null; // ?? + reweight: null; // ?? + leaderboardGroup: null; // ?? + plays: number; + clan: null; // ?? + clanRankingContested: boolean; +}; diff --git a/src/schemas/beatleader/metadata.ts b/src/schemas/beatleader/metadata.ts new file mode 100644 index 0000000..15e27de --- /dev/null +++ b/src/schemas/beatleader/metadata.ts @@ -0,0 +1,5 @@ +export type BeatleaderMetadata = { + itemsPerPage: number; + page: number; + total: number; +}; diff --git a/src/schemas/beatleader/modifierRating.ts b/src/schemas/beatleader/modifierRating.ts new file mode 100644 index 0000000..f1a21e8 --- /dev/null +++ b/src/schemas/beatleader/modifierRating.ts @@ -0,0 +1,18 @@ +export type BeatleaderModifierRating = { + id: number; + fsPredictedAcc: number; + fsPassRating: number; + fsAccRating: number; + fsTechRating: number; + fsStars: number; + ssPredictedAcc: number; + ssPassRating: number; + ssAccRating: number; + ssTechRating: number; + ssStars: number; + sfPredictedAcc: number; + sfPassRating: number; + sfAccRating: number; + sfTechRating: number; + sfStars: number; +}; diff --git a/src/schemas/beatleader/modifiers.ts b/src/schemas/beatleader/modifiers.ts new file mode 100644 index 0000000..38d45ab --- /dev/null +++ b/src/schemas/beatleader/modifiers.ts @@ -0,0 +1,16 @@ +export type BeatleaderModifier = { + modifierId: number; + da: number; + fs: number; + sf: number; + ss: number; + gn: number; + na: number; + nb: number; + nf: number; + no: number; + pm: number; + sc: number; + sa: number; + op: number; +}; diff --git a/src/schemas/beatleader/score.ts b/src/schemas/beatleader/score.ts new file mode 100644 index 0000000..52321db --- /dev/null +++ b/src/schemas/beatleader/score.ts @@ -0,0 +1,51 @@ +import { BeatleaderLeaderboard } from "./leaderboard"; +import { BeatleaderScoreImprovement } from "./scoreImprovement"; +import { BeatleaderScoreOffsets } from "./scoreOffsets"; + +export type BeatleaderScore = { + myScore: null; // ?? + validContexts: number; + leaderboard: BeatleaderLeaderboard; + contextExtensions: null; // ?? + accLeft: number; + accRight: number; + id: number; + baseScore: number; + modifiedScore: number; + accuracy: number; + playerId: string; + pp: number; + bonusPp: number; + passPP: number; + accPP: number; + techPP: number; + rank: number; + country: string; + fcAccuracy: number; + fcPp: number; + weight: number; + replay: string; + modifiers: string; + badCuts: number; + missedNotes: number; + bombCuts: number; + wallsHit: number; + pauses: number; + fullCombo: boolean; + platform: string; + maxCombo: number; + maxStreak: number; + hmd: number; + controller: number; + leaderboardId: string; + timeset: string; + timepost: number; + replaysWatched: number; + playCount: number; + priority: number; + player: null; // ?? + scoreImprovement: BeatleaderScoreImprovement; + rankVoting: null; // ?? + metadata: null; // ?? + offsets: BeatleaderScoreOffsets; +}; diff --git a/src/schemas/beatleader/scoreImprovement.ts b/src/schemas/beatleader/scoreImprovement.ts new file mode 100644 index 0000000..a20b54a --- /dev/null +++ b/src/schemas/beatleader/scoreImprovement.ts @@ -0,0 +1,19 @@ +export type BeatleaderScoreImprovement = { + id: number; + timeset: number; + score: number; + accuracy: number; + pp: number; + bonusPp: number; + rank: number; + accRight: number; + accLeft: number; + averageRankedAccuracy: number; + totalPp: number; + totalRank: number; + badCuts: number; + missedNotes: number; + bombCuts: number; + wallsHit: number; + pauses: number; +}; diff --git a/src/schemas/beatleader/scoreOffsets.ts b/src/schemas/beatleader/scoreOffsets.ts new file mode 100644 index 0000000..d443d40 --- /dev/null +++ b/src/schemas/beatleader/scoreOffsets.ts @@ -0,0 +1,8 @@ +export type BeatleaderScoreOffsets = { + id: number; + frames: number; + notes: number; + walls: number; + heights: number; + pauses: number; +}; diff --git a/src/schemas/beatleader/scores.ts b/src/schemas/beatleader/scores.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/schemas/beatleader/smaller/smallerLeaderboard.ts b/src/schemas/beatleader/smaller/smallerLeaderboard.ts new file mode 100644 index 0000000..584827f --- /dev/null +++ b/src/schemas/beatleader/smaller/smallerLeaderboard.ts @@ -0,0 +1,5 @@ +import { BeatleaderSmallerSong } from "./smallerSong"; + +export type BeatleaderSmallerLeaderboard = { + song: BeatleaderSmallerSong; +}; diff --git a/src/schemas/beatleader/smaller/smallerScore.ts b/src/schemas/beatleader/smaller/smallerScore.ts new file mode 100644 index 0000000..9e21033 --- /dev/null +++ b/src/schemas/beatleader/smaller/smallerScore.ts @@ -0,0 +1,14 @@ +import { BeatleaderSmallerLeaderboard } from "./smallerLeaderboard"; +import { BeatleaderSmallerScoreImprovement } from "./smallerScoreImprovement"; + +export type BeatleaderSmallerScore = { + id: number; + timepost: number; + accLeft: number; + accRight: number; + fcAccuracy: number; + wallsHit: number; + replay: string; + leaderboard: BeatleaderSmallerLeaderboard; + scoreImprovement: BeatleaderSmallerScoreImprovement | null; +}; diff --git a/src/schemas/beatleader/smaller/smallerScoreImprovement.ts b/src/schemas/beatleader/smaller/smallerScoreImprovement.ts new file mode 100644 index 0000000..2c32ce3 --- /dev/null +++ b/src/schemas/beatleader/smaller/smallerScoreImprovement.ts @@ -0,0 +1,9 @@ +export type BeatleaderSmallerScoreImprovement = { + score: number; + accuracy: number; + accRight: number; + accLeft: number; + badCuts: number; + missedNotes: number; + bombCuts: number; +}; diff --git a/src/schemas/beatleader/smaller/smallerSong.ts b/src/schemas/beatleader/smaller/smallerSong.ts new file mode 100644 index 0000000..3b8811c --- /dev/null +++ b/src/schemas/beatleader/smaller/smallerSong.ts @@ -0,0 +1,4 @@ +export type BeatleaderSmallerSong = { + hash: string; + bpm: number; +}; diff --git a/src/schemas/beatleader/song.ts b/src/schemas/beatleader/song.ts new file mode 100644 index 0000000..51c8898 --- /dev/null +++ b/src/schemas/beatleader/song.ts @@ -0,0 +1,16 @@ +export type BeatleaderSong = { + id: string; + hash: string; + name: string; + subName: string; + author: string; + mapperId: string; + coverImage: string; + fullCoverImage: string; + downloadUrl: string; + bpm: number; + duration: number; + tags: string; + uploadTime: number; + difficulties: null; // ?? +}; diff --git a/src/schemas/scoresaber/smaller/smallerLeaderboard.ts b/src/schemas/scoresaber/smaller/smallerLeaderboard.ts new file mode 100644 index 0000000..378ae5c --- /dev/null +++ b/src/schemas/scoresaber/smaller/smallerLeaderboard.ts @@ -0,0 +1,12 @@ +import { ScoresaberDifficulty } from "../difficulty"; + +export type ScoresaberSmallerLeaderboardInfo = { + id: string; + songHash: string; + difficulty: ScoresaberDifficulty; + maxScore: number; + createdDate: string; + stars: number; + plays: number; + coverImage: string; +}; diff --git a/src/schemas/scoresaber/smaller/smallerPlayerScore.ts b/src/schemas/scoresaber/smaller/smallerPlayerScore.ts new file mode 100644 index 0000000..73fb629 --- /dev/null +++ b/src/schemas/scoresaber/smaller/smallerPlayerScore.ts @@ -0,0 +1,7 @@ +import { ScoresaberSmallerLeaderboardInfo } from "./smallerLeaderboard"; +import { ScoresaberSmallerScore } from "./smallerScore"; + +export type ScoresaberSmallerPlayerScore = { + score: ScoresaberSmallerScore; + leaderboard: ScoresaberSmallerLeaderboardInfo; +}; diff --git a/src/schemas/scoresaber/smaller/smallerScore.ts b/src/schemas/scoresaber/smaller/smallerScore.ts new file mode 100644 index 0000000..842fd91 --- /dev/null +++ b/src/schemas/scoresaber/smaller/smallerScore.ts @@ -0,0 +1,16 @@ +export type ScoresaberSmallerScore = { + id: number; + rank: number; + baseScore: number; + modifiedScore: number; + pp: number; + weight: number; + modifiers: string; + multiplier: number; + badCuts: number; + missedNotes: number; + maxCombo: number; + fullCombo: boolean; + hmd: number; + timeSet: string; +}; diff --git a/src/store/beatLeaderScoresStore.ts b/src/store/beatLeaderScoresStore.ts new file mode 100644 index 0000000..3119100 --- /dev/null +++ b/src/store/beatLeaderScoresStore.ts @@ -0,0 +1,347 @@ +"use client"; + +import { BeatleaderSmallerScore } from "@/schemas/beatleader/smaller/smallerScore"; +import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; +import { BeatLeaderAPI } from "@/utils/beatleader/api"; +import moment from "moment"; +import { toast } from "react-toastify"; +import { create } from "zustand"; +import { createJSONStorage, persist } from "zustand/middleware"; +import { useSettingsStore } from "./settingsStore"; + +type Player = { + id: string; + scores: BeatleaderSmallerScore[]; +}; + +interface BeatLeaderScoresStore { + lastUpdated: number; + players: Player[]; + + /** + * Sets when the player scores were last updated + * + * @param lastUpdated when the player scores were last updated + */ + setLastUpdated: (lastUpdated: number) => void; + + /** + * Checks if the player exists + * + * @param playerId the player id + * @returns if the player exists + */ + exists: (playerId: string) => boolean; + + /** + * Gets the given player + * + * @param playerId the player id + * @returns the player + */ + get(playerId: string): Player | undefined; + + /** + * Gets the score for the given player and song hash + * + * @param playerId the player id + * @param songHash the song hash + */ + getScore( + playerId: string, + songHash: string, + ): BeatleaderSmallerScore | undefined; + + /** + * Adds the player to the local database + * + * @param playerId the player id + * @param callback a callback to call when a score page is fetched + * @returns if the player was added successfully + */ + addPlayer: ( + playerId: string, + callback?: (page: number, totalPages: number) => void, + ) => Promise<{ + error: boolean; + message: string; + }>; + + /** + * Refreshes the player scores and adds any new scores to the local database + */ + updatePlayerScores: () => void; +} + +const UPDATE_INTERVAL = 1000 * 60 * 30; // 30 minutes + +export const useBeatLeaderScoresStore = create()( + persist( + (set) => ({ + lastUpdated: 0, + players: [], + + setLastUpdated: (lastUpdated: number) => { + set({ lastUpdated }); + }, + + exists: (playerId: string) => { + const players: Player[] = useBeatLeaderScoresStore.getState().players; + return players.some((player) => player.id == playerId); + }, + + get: (playerId: string) => { + const players: Player[] = useBeatLeaderScoresStore.getState().players; + return players.find((player) => player.id == playerId); + }, + + getScore: (playerId: string, songHash: string) => { + const player = useBeatLeaderScoresStore.getState().get(playerId); + if (player == undefined) return undefined; + + return player.scores.find( + (score) => score.leaderboard.song.hash == songHash, + ); + }, + + addPlayer: async ( + playerId: string, + callback?: ( + page: number, + totalPages: number, + leaderboardName: string, + ) => void, + ) => { + const players = useBeatLeaderScoresStore.getState().players; + + // Check if the player already exists + if (useBeatLeaderScoresStore.getState().exists(playerId)) { + return { + error: true, + message: "Player already exists", + }; + } + + // Get all of the players scores + const scores = await BeatLeaderAPI.fetchAllScores( + playerId, + (page, totalPages) => { + if (callback) callback(page, totalPages, "BeatLeader"); + }, + ); + if (scores == undefined) { + return { + error: true, + message: "Could not fetch beatleader scores for player", + }; + } + let smallerScores = new Array(); + for (const score of scores) { + // We have to do this to limit the amount of data we store + // so we don't exceed the local storage limit + smallerScores.push({ + id: score.id, + accLeft: score.accLeft, + accRight: score.accRight, + fcAccuracy: score.fcAccuracy, + wallsHit: score.wallsHit, + replay: score.replay, + leaderboard: { + song: { + bpm: score.leaderboard.song.bpm, + hash: score.leaderboard.song.hash, + }, + }, + scoreImprovement: + score.scoreImprovement != null + ? { + score: score.scoreImprovement.score, + accuracy: score.scoreImprovement.accuracy, + accRight: score.scoreImprovement.accRight, + accLeft: score.scoreImprovement.accLeft, + badCuts: score.scoreImprovement.badCuts, + missedNotes: score.scoreImprovement.missedNotes, + bombCuts: score.scoreImprovement.bombCuts, + } + : null, + timepost: score.timepost, + }); + } + + // Remove scores that are already in the database + const player = useBeatLeaderScoresStore.getState().get(playerId); + if (player) { + smallerScores = smallerScores.filter( + (score) => player.scores.findIndex((s) => s.id == score.id) == -1, + ); + } + + set({ + lastUpdated: Date.now(), + players: [ + ...players, + { + id: playerId, + scores: smallerScores, + }, + ], + }); + return { + error: false, + message: "Player added successfully", + }; + }, + + updatePlayerScores: async () => { + const players = useBeatLeaderScoresStore.getState().players; + const friends = useSettingsStore.getState().friends; + + let allPlayers = new Array(); + for (const friend of friends) { + allPlayers.push(friend); + } + const localPlayer = useSettingsStore.getState().player; + if (localPlayer) { + allPlayers.push(localPlayer); + } + + // add local player and friends if they don't exist + for (const player of allPlayers) { + if (useBeatLeaderScoresStore.getState().get(player.id) == undefined) { + toast.info( + `${ + player.id == localPlayer?.id + ? `You were` + : `Friend ${player.name} was` + } missing from the BeatLeader scores database, adding...`, + ); + await useBeatLeaderScoresStore.getState().addPlayer(player.id); + toast.success( + `${ + player.id == useSettingsStore.getState().player?.id + ? `You were` + : `Friend ${player.name} was` + } added to the BeatLeader scores database`, + ); + } + } + + // Skip if we refreshed the scores recently + const timeUntilRefreshMs = + UPDATE_INTERVAL - + (Date.now() - useBeatLeaderScoresStore.getState().lastUpdated); + if (timeUntilRefreshMs > 0) { + console.log( + "Waiting", + moment.duration(timeUntilRefreshMs).humanize(), + "to refresh scores for players", + ); + setTimeout( + () => useBeatLeaderScoresStore.getState().updatePlayerScores(), + timeUntilRefreshMs, + ); + return; + } + + // loop through all of the players and update their scores + for (const player of players) { + if (player == undefined) continue; + console.log(`Updating scores for ${player.id}...`); + + let newPlayers = players; + let oldScores = player.scores; + + // Sort the scores by date (newset to oldest), so we know when to stop searching for new scores + oldScores = oldScores.sort((a, b) => { + return a.timepost - b.timepost; + }); + if (!oldScores.length) return; + + const mostRecentScore = oldScores[0]; + let search = true; + + let page = 0; + let newScoresCount = 0; + while (search) { + page++; + const newScores = await BeatLeaderAPI.fetchScores(player.id, page); + if (newScores == undefined) continue; + + for (const score of newScores.scores) { + if (mostRecentScore && score.id == mostRecentScore.id) { + search = false; + break; + } + + // remove the old score + const oldScoreIndex = oldScores.findIndex( + (score) => score.id == score.id, + ); + if (oldScoreIndex != -1) { + oldScores = oldScores.splice(oldScoreIndex, 1); + } + oldScores.push({ + id: score.id, + accLeft: score.accLeft, + accRight: score.accRight, + fcAccuracy: score.fcAccuracy, + wallsHit: score.wallsHit, + replay: score.replay, + leaderboard: { + song: { + bpm: score.leaderboard.song.bpm, + hash: score.leaderboard.song.hash, + }, + }, + scoreImprovement: + score.scoreImprovement != null + ? { + score: score.scoreImprovement.score, + accuracy: score.scoreImprovement.accuracy, + accRight: score.scoreImprovement.accRight, + accLeft: score.scoreImprovement.accLeft, + badCuts: score.scoreImprovement.badCuts, + missedNotes: score.scoreImprovement.missedNotes, + bombCuts: score.scoreImprovement.bombCuts, + } + : null, + timepost: score.timepost, + }); + newScoresCount++; + } + } + + // Remove the player if it already exists + newPlayers = newPlayers.filter((playerr) => playerr.id != player.id); + // Add the player + newPlayers.push({ + id: player.id, + scores: oldScores, + }); + + if (newScoresCount > 0) { + console.log( + `Found ${newScoresCount} new beatleader scores for ${player.id}`, + ); + } + + set({ + players: newPlayers, + lastUpdated: Date.now(), + }); + console.log(friends); + } + }, + }), + { + name: "beatleaderScores", + storage: createJSONStorage(() => localStorage), + version: 2, + + migrate: (state: any, version: number) => { + state.scores = []; + return state; + }, + }, + ), +); diff --git a/src/store/playerScoresStore.ts b/src/store/scoresaberScoresStore.ts similarity index 61% rename from src/store/playerScoresStore.ts rename to src/store/scoresaberScoresStore.ts index cddd717..99c8664 100644 --- a/src/store/playerScoresStore.ts +++ b/src/store/scoresaberScoresStore.ts @@ -1,8 +1,8 @@ "use client"; import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; -import { ScoresaberPlayerScore } from "@/schemas/scoresaber/playerScore"; -import { fetchAllScores, fetchScores } from "@/utils/scoresaber/api"; +import { ScoresaberSmallerPlayerScore } from "@/schemas/scoresaber/smaller/smallerPlayerScore"; +import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import moment from "moment"; import { toast } from "react-toastify"; import { create } from "zustand"; @@ -12,11 +12,11 @@ import { useSettingsStore } from "./settingsStore"; type Player = { id: string; scores: { - scoresaber: ScoresaberPlayerScore[]; + scoresaber: ScoresaberSmallerPlayerScore[]; }; }; -interface PlayerScoresStore { +interface ScoreSaberScoresStore { lastUpdated: number; players: Player[]; @@ -66,7 +66,7 @@ interface PlayerScoresStore { const UPDATE_INTERVAL = 1000 * 60 * 30; // 30 minutes -export const usePlayerScoresStore = create()( +export const useScoresaberScoresStore = create()( persist( (set) => ({ lastUpdated: 0, @@ -77,12 +77,12 @@ export const usePlayerScoresStore = create()( }, exists: (playerId: string) => { - const players: Player[] = usePlayerScoresStore.getState().players; + const players: Player[] = useScoresaberScoresStore.getState().players; return players.some((player) => player.id == playerId); }, get: (playerId: string) => { - const players: Player[] = usePlayerScoresStore.getState().players; + const players: Player[] = useScoresaberScoresStore.getState().players; return players.find((player) => player.id == playerId); }, @@ -90,10 +90,10 @@ export const usePlayerScoresStore = create()( playerId: string, callback?: (page: number, totalPages: number) => void, ) => { - const players = usePlayerScoresStore.getState().players; + const players = useScoresaberScoresStore.getState().players; // Check if the player already exists - if (usePlayerScoresStore.getState().exists(playerId)) { + if (useScoresaberScoresStore.getState().exists(playerId)) { return { error: true, message: "Player already exists", @@ -101,23 +101,53 @@ export const usePlayerScoresStore = create()( } // Get all of the players scores - let scores = await fetchAllScores( + let scores = await ScoreSaberAPI.fetchAllScores( playerId, "recent", (page, totalPages) => { if (callback) callback(page, totalPages); }, ); - if (scores == undefined) { return { error: true, message: "Could not fetch scores for player", }; } + let smallerScores = new Array(); + for (const score of scores) { + smallerScores.push({ + score: { + id: score.score.id, + rank: score.score.rank, + baseScore: score.score.baseScore, + modifiedScore: score.score.modifiedScore, + pp: score.score.pp, + weight: score.score.weight, + modifiers: score.score.modifiers, + multiplier: score.score.multiplier, + badCuts: score.score.badCuts, + missedNotes: score.score.missedNotes, + maxCombo: score.score.maxCombo, + fullCombo: score.score.fullCombo, + hmd: score.score.hmd, + timeSet: score.score.timeSet, + }, + leaderboard: { + id: score.leaderboard.id, + songHash: score.leaderboard.songHash, + difficulty: score.leaderboard.difficulty, + maxScore: score.leaderboard.maxScore, + createdDate: score.leaderboard.createdDate, + stars: score.leaderboard.stars, + plays: score.leaderboard.plays, + coverImage: score.leaderboard.coverImage, + }, + }); + } // Remove scores that are already in the database - const player = usePlayerScoresStore.getState().get(playerId); + const player = useScoresaberScoresStore.getState().get(playerId); if (player) { scores = scores.filter( (score) => @@ -145,7 +175,7 @@ export const usePlayerScoresStore = create()( }, updatePlayerScores: async () => { - const players = usePlayerScoresStore.getState().players; + const players = useScoresaberScoresStore.getState().players; const friends = useSettingsStore.getState().friends; let allPlayers = new Array(); @@ -159,21 +189,21 @@ export const usePlayerScoresStore = create()( // add local player and friends if they don't exist for (const player of allPlayers) { - if (usePlayerScoresStore.getState().get(player.id) == undefined) { + if (useScoresaberScoresStore.getState().get(player.id) == undefined) { toast.info( `${ player.id == localPlayer?.id ? `You were` : `Friend ${player.name} was` - } missing from the scores database, adding...`, + } missing from the ScoreSaber scores database, adding...`, ); - await usePlayerScoresStore.getState().addPlayer(player.id); + await useScoresaberScoresStore.getState().addPlayer(player.id); toast.success( `${ player.id == useSettingsStore.getState().player?.id ? `You were` : `Friend ${player.name} was` - } added to the scores database`, + } added to the ScoreSaber scores database`, ); } } @@ -181,7 +211,7 @@ export const usePlayerScoresStore = create()( // Skip if we refreshed the scores recently const timeUntilRefreshMs = UPDATE_INTERVAL - - (Date.now() - usePlayerScoresStore.getState().lastUpdated); + (Date.now() - useScoresaberScoresStore.getState().lastUpdated); if (timeUntilRefreshMs > 0) { console.log( "Waiting", @@ -189,12 +219,13 @@ export const usePlayerScoresStore = create()( "to refresh scores for players", ); setTimeout( - () => usePlayerScoresStore.getState().updatePlayerScores(), + () => useScoresaberScoresStore.getState().updatePlayerScores(), timeUntilRefreshMs, ); return; } + // loop through all of the players and update their scores for (const player of players) { if (player == undefined) continue; console.log(`Updating scores for ${player.id}...`); @@ -216,23 +247,50 @@ export const usePlayerScoresStore = create()( let newScoresCount = 0; while (search) { page++; - const newScores = await fetchScores(player.id, page); + const newScores = await ScoreSaberAPI.fetchScores(player.id, page); if (newScores == undefined) continue; - for (const newScore of newScores.scores) { - if (mostRecentScore && newScore.score.id == mostRecentScore.id) { + for (const score of newScores.scores) { + if (mostRecentScore && score.score.id == mostRecentScore.id) { search = false; break; } // remove the old score const oldScoreIndex = oldScores.findIndex( - (score) => score.score.id == newScore.score.id, + (score) => score.score.id == score.score.id, ); if (oldScoreIndex != -1) { oldScores = oldScores.splice(oldScoreIndex, 1); } - oldScores.push(newScore); + oldScores.push({ + score: { + id: score.score.id, + rank: score.score.rank, + baseScore: score.score.baseScore, + modifiedScore: score.score.modifiedScore, + pp: score.score.pp, + weight: score.score.weight, + modifiers: score.score.modifiers, + multiplier: score.score.multiplier, + badCuts: score.score.badCuts, + missedNotes: score.score.missedNotes, + maxCombo: score.score.maxCombo, + fullCombo: score.score.fullCombo, + hmd: score.score.hmd, + timeSet: score.score.timeSet, + }, + leaderboard: { + id: score.leaderboard.id, + songHash: score.leaderboard.songHash, + difficulty: score.leaderboard.difficulty, + maxScore: score.leaderboard.maxScore, + createdDate: score.leaderboard.createdDate, + stars: score.leaderboard.stars, + plays: score.leaderboard.plays, + coverImage: score.leaderboard.coverImage, + }, + }); newScoresCount++; } } @@ -261,7 +319,7 @@ export const usePlayerScoresStore = create()( }, }), { - name: "playerScores", + name: "scoresaberScores", storage: createJSONStorage(() => localStorage), version: 1, @@ -282,9 +340,3 @@ export const usePlayerScoresStore = create()( }, ), ); - -// Update the player scores every 30 minutes -usePlayerScoresStore.getState().updatePlayerScores(); -setInterval(() => { - usePlayerScoresStore.getState().updatePlayerScores(); -}, UPDATE_INTERVAL); diff --git a/src/store/settingsStore.ts b/src/store/settingsStore.ts index 81813f0..4d9db05 100644 --- a/src/store/settingsStore.ts +++ b/src/store/settingsStore.ts @@ -2,7 +2,7 @@ import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { SortType, SortTypes } from "@/types/SortTypes"; -import { getPlayerInfo } from "@/utils/scoresaber/api"; +import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import moment from "moment"; import { create } from "zustand"; import { createJSONStorage, persist } from "zustand/middleware"; @@ -48,7 +48,7 @@ export const useSettingsStore = create()( return false; } - const friend = await getPlayerInfo(friendId); + const friend = await ScoreSaberAPI.getPlayerInfo(friendId); if (friend == undefined || friend == null) return false; set({ friends: [...friends, friend] }); diff --git a/src/utils/beatleader/api.ts b/src/utils/beatleader/api.ts new file mode 100644 index 0000000..666f38d --- /dev/null +++ b/src/utils/beatleader/api.ts @@ -0,0 +1,102 @@ +import { BeatleaderScore } from "@/schemas/beatleader/score"; +import { ssrSettings } from "@/ssrSettings"; +import { FetchQueue } from "../fetchWithQueue"; +import { formatString } from "../string"; + +// Create a fetch instance with a cache +const fetchQueue = new FetchQueue(); + +// Api endpoints +const API_URL = ssrSettings.proxy + "/https://api.beatleader.xyz"; +const PLAYER_SCORES_URL = + API_URL + "/player/{}/scores?sortBy=date&order=0&page={}&count=100"; + +/** + * Get the players scores from the given page + * + * @param playerId the id of the player + * @param page the page to get the scores from + * @param searchType the type of search to perform + * @param limit the limit of scores to get + * @returns a list of scores + */ +async function fetchScores( + playerId: string, + page: number = 1, + limit: number = 100, +): Promise< + | { + scores: BeatleaderScore[]; + pageInfo: { + totalScores: number; + page: number; + totalPages: number; + }; + } + | undefined +> { + if (limit > 100) { + throw new Error("Limit cannot be greater than 100"); + } + + const response = await fetchQueue.fetch( + formatString(PLAYER_SCORES_URL, true, playerId, page), + ); + const json = await response.json(); + + // Check if there was an error fetching the user data + console.log(json); + + const metadata = json.metadata; + return { + scores: json.data as BeatleaderScore[], + pageInfo: { + totalScores: json.totalScores, + page: json.page, + totalPages: Math.ceil(json.totalScores / metadata.itemsPerPage), + }, + }; +} + +/** + * Gets all of the players for the given player id + * + * @param playerId the id of the player + * @param searchType the type of search to perform + * @param callback a callback to call when a page is fetched + * @returns a list of scores + */ +async function fetchAllScores( + playerId: string, + callback?: (currentPage: number, totalPages: number) => void, +): Promise { + const scores = new Array(); + + let done = false, + page = 1; + do { + const response = await fetchScores(playerId, page); + if (response == undefined) { + done = true; + break; + } + const { scores: scoresFetched } = response; + if (scoresFetched.length === 0) { + done = true; + break; + } + scores.push(...scoresFetched); + + if (callback) { + callback(page, response.pageInfo.totalPages); + } + page++; + } while (!done); + + return scores as BeatleaderScore[]; +} + +export const BeatLeaderAPI = { + fetchScores, + fetchAllScores, +}; diff --git a/src/utils/fetchWithQueue.ts b/src/utils/fetchWithQueue.ts index 4d97513..7601728 100644 --- a/src/utils/fetchWithQueue.ts +++ b/src/utils/fetchWithQueue.ts @@ -25,7 +25,18 @@ export class FetchQueue { const response = await fetch(url); if (response.status === 429) { - const retryAfter = Number(response.headers.get("retry-after")) * 1000; + const hasRetryAfter = response.headers.has("retry-after"); + let retryAfter = + Number( + hasRetryAfter + ? response.headers.get("retry-after") + : new Date( + response.headers.get("X-Rate-Limit-Reset") as string, + ).getTime() / 1000, + ) * 1000; + if (!retryAfter) { + retryAfter = 3_000; // default to 3 seconds if we can't get the reset time + } this._queue.push(url); await new Promise((resolve) => setTimeout(resolve, retryAfter)); return this.fetch(this._queue.shift() as string); diff --git a/src/utils/scoresaber/api.ts b/src/utils/scoresaber/api.ts index 63ff42c..86a2810 100644 --- a/src/utils/scoresaber/api.ts +++ b/src/utils/scoresaber/api.ts @@ -28,7 +28,7 @@ const SearchType = { * @param name the name to search * @returns a list of players */ -export async function searchByName( +async function searchByName( name: string, ): Promise { const response = await fetchQueue.fetch( @@ -50,7 +50,7 @@ export async function searchByName( * @param playerId the id of the player * @returns the player info */ -export async function getPlayerInfo( +async function getPlayerInfo( playerId: string, ): Promise { const response = await fetchQueue.fetch( @@ -75,7 +75,7 @@ export async function getPlayerInfo( * @param limit the limit of scores to get * @returns a list of scores */ -export async function fetchScores( +async function fetchScores( playerId: string, page: number = 1, searchType: string = SearchType.RECENT, @@ -92,10 +92,7 @@ export async function fetchScores( | undefined > { if (limit > 100) { - console.log( - "Scoresaber API only allows a limit of 100 scores per request, limiting to 100.", - ); - limit = 100; + throw new Error("Limit cannot be greater than 100"); } const response = await fetchQueue.fetch( formatString(PLAYER_SCORES, true, playerId, limit, searchType, page), @@ -127,7 +124,7 @@ export async function fetchScores( * @param callback a callback to call when a page is fetched * @returns a list of scores */ -export async function fetchAllScores( +async function fetchAllScores( playerId: string, searchType: string, callback?: (currentPage: number, totalPages: number) => void, @@ -165,7 +162,7 @@ export async function fetchAllScores( * @param country the country to get the players from * @returns a list of players */ -export async function fetchTopPlayers( +async function fetchTopPlayers( page: number = 1, country?: string, ): Promise< @@ -201,3 +198,11 @@ export async function fetchTopPlayers( }, }; } + +export const ScoreSaberAPI = { + searchByName, + getPlayerInfo, + fetchScores, + fetchAllScores, + fetchTopPlayers, +}; diff --git a/src/utils/scoresaber/scores.ts b/src/utils/scoresaber/scores.ts index 4919ae5..12059f9 100644 --- a/src/utils/scoresaber/scores.ts +++ b/src/utils/scoresaber/scores.ts @@ -1,7 +1,7 @@ // Yoinked from https://github.com/Shurdoof/pp-calculator/blob/c24b5ca452119339928831d74e6d603fb17fd5ef/src/lib/pp/calculator.ts // Thank for for this I have no fucking idea what the maths is doing but it works! -import { usePlayerScoresStore } from "@/store/playerScoresStore"; +import { useScoresaberScoresStore } from "@/store/scoresaberScoresStore"; export const WEIGHT_COEFFICIENT = 0.965; @@ -115,7 +115,7 @@ function calcRawPpAtIdx( * @returns the pp boundary (+ per raw pp) */ export function calcPpBoundary(playerId: string, expectedPp = 1) { - const rankedScores = usePlayerScoresStore + const rankedScores = useScoresaberScoresStore .getState() .players.find((p) => p.id === playerId) ?.scores?.scoresaber.filter((s) => s.score.pp !== undefined); @@ -156,7 +156,7 @@ export function calcPpBoundary(playerId: string, expectedPp = 1) { * @returns the highest pp play */ export function getHighestPpPlay(playerId: string) { - const rankedScores = usePlayerScoresStore + const rankedScores = useScoresaberScoresStore .getState() .players.find((p) => p.id === playerId) ?.scores?.scoresaber.filter((s) => s.score.pp !== undefined); @@ -176,7 +176,7 @@ export function getHighestPpPlay(playerId: string) { * @param limit the amount of top scores to average (default: 20) */ export function getAveragePp(playerId: string, limit: number = 20) { - const rankedScores = usePlayerScoresStore + const rankedScores = useScoresaberScoresStore .getState() .players.find((p) => p.id === playerId) ?.scores?.scoresaber.filter((s) => s.score.pp !== undefined);