diff --git a/next.config.js b/next.config.js index bca61d9..776a365 100644 --- a/next.config.js +++ b/next.config.js @@ -43,6 +43,12 @@ const nextConfig = { port: "", pathname: "/**", }, + { + protocol: "https", + hostname: "avatars.akamai.steamstatic.com", + port: "", + pathname: "/**", + }, ], }, }; diff --git a/public/assets/logos/beatleader.png b/public/assets/logos/beatleader.png new file mode 100644 index 0000000..9cf241b Binary files /dev/null and b/public/assets/logos/beatleader.png differ diff --git a/public/assets/logos/scoresaber.png b/public/assets/logos/scoresaber.png new file mode 100644 index 0000000..47c058e Binary files /dev/null and b/public/assets/logos/scoresaber.png differ diff --git a/src/app/overlay/builder/page.tsx b/src/app/overlay/builder/page.tsx index 93d4530..fda77d7 100644 --- a/src/app/overlay/builder/page.tsx +++ b/src/app/overlay/builder/page.tsx @@ -79,10 +79,10 @@ export default function Analytics() { id: "scoresaber", value: "ScoreSaber", }, - // { - // id: "beatleader", - // value: "BeatLeader", - // }, + { + id: "beatleader", + value: "BeatLeader", + }, ]} onChange={(value) => { settingsStore.setPlatform(value); diff --git a/src/app/overlay/page.tsx b/src/app/overlay/page.tsx index b400ac6..e892e26 100644 --- a/src/app/overlay/page.tsx +++ b/src/app/overlay/page.tsx @@ -7,7 +7,8 @@ import ScoreStats from "@/components/overlay/ScoreStats"; import SongInfo from "@/components/overlay/SongInfo"; import { Card, CardDescription, CardTitle } from "@/components/ui/card"; import { HttpSiraStatus } from "@/overlay/httpSiraStatus"; -import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; +import { OverlayPlayer } from "@/overlay/type/overlayPlayer"; +import { BeatLeaderAPI } from "@/utils/beatleader/api"; import { ScoreSaberAPI } from "@/utils/scoresaber/api"; import { Component } from "react"; @@ -17,7 +18,7 @@ interface OverlayProps {} interface OverlayState { mounted: boolean; - player: ScoresaberPlayer | undefined; + player: OverlayPlayer | undefined; settings: any | undefined; } @@ -31,13 +32,44 @@ export default class Overlay extends Component { }; } - updatePlayer = async (playerId: string) => { + updatePlayer = async ( + playerId: string, + leaderboard: "scoresaber" | "beatleader" = "scoresaber", + ) => { console.log(`Updating player stats for ${playerId}`); - const player = await ScoreSaberAPI.fetchPlayerData(playerId); - if (!player) { - return; + if (leaderboard == "scoresaber") { + const player = await ScoreSaberAPI.fetchPlayerData(playerId); + if (!player) { + return; + } + this.setState({ + player: { + id: player.id, + profilePicture: player.profilePicture, + country: player.country, + pp: player.pp, + rank: player.rank, + countryRank: player.countryRank, + }, + }); + } + + if (leaderboard == "beatleader") { + const player = await BeatLeaderAPI.fetchPlayerData(playerId); + if (!player) { + return; + } + this.setState({ + player: { + id: player.id, + profilePicture: player.avatar, + country: player.country, + pp: player.pp, + rank: player.rank, + countryRank: player.countryRank, + }, + }); } - this.setState({ player }); }; componentDidMount() { @@ -60,9 +92,9 @@ export default class Overlay extends Component { this.setState({ settings: settings }); if (settings.settings.showPlayerStats) { - this.updatePlayer(settings.accountId); + this.updatePlayer(settings.accountId, settings.platform); setInterval(() => { - this.updatePlayer(settings.accountId); + this.updatePlayer(settings.accountId, settings.platform); }, UPDATE_INTERVAL); } } @@ -113,7 +145,7 @@ export default class Overlay extends Component {
{this.state.settings.settings.showPlayerStats && player && ( - + )} {this.state.settings.settings.showScoreStats && }
diff --git a/src/components/overlay/PlayerStats.tsx b/src/components/overlay/PlayerStats.tsx index d9c186e..55dad76 100644 --- a/src/components/overlay/PlayerStats.tsx +++ b/src/components/overlay/PlayerStats.tsx @@ -1,14 +1,20 @@ -import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; +import { OverlayPlayer } from "@/overlay/type/overlayPlayer"; import { formatNumber } from "@/utils/numberUtils"; import { GlobeAltIcon } from "@heroicons/react/20/solid"; import Image from "next/image"; import CountyFlag from "../CountryFlag"; type PlayerStatsProps = { - player: ScoresaberPlayer; + player: OverlayPlayer; + settings: any; }; -export default function PlayerStats({ player }: PlayerStatsProps) { +const leaderboardImages: Record = { + scoresaber: "/assets/logos/scoresaber.png", + beatleader: "/assets/logos/beatleader.png", +}; + +export default function PlayerStats({ player, settings }: PlayerStatsProps) { return (
-

{formatNumber(player.pp, 2)}pp

+
+ Leaderboard logo +

{formatNumber(player.pp, 2)}pp

+

#{formatNumber(player.rank)}

diff --git a/src/overlay/type/overlayPlayer.ts b/src/overlay/type/overlayPlayer.ts new file mode 100644 index 0000000..c043797 --- /dev/null +++ b/src/overlay/type/overlayPlayer.ts @@ -0,0 +1,8 @@ +export type OverlayPlayer = { + id: string; + country: string; + profilePicture: string; + pp: number; + rank: number; + countryRank: number; +}; 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/player.ts b/src/schemas/beatleader/player.ts new file mode 100644 index 0000000..b669254 --- /dev/null +++ b/src/schemas/beatleader/player.ts @@ -0,0 +1,9 @@ +export type BeatLeaderPlayer = { + id: string; + country: string; + avatar: string; + pp: number; + rank: number; + countryRank: number; + // todo: finish this +}; 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/utils/beatleader/api.ts b/src/utils/beatleader/api.ts new file mode 100644 index 0000000..9f4a2f2 --- /dev/null +++ b/src/utils/beatleader/api.ts @@ -0,0 +1,126 @@ +import { BeatLeaderPlayer } from "@/schemas/beatleader/player"; +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"; +const PLAYER_URL = API_URL + "/player/{}?stats=false"; + +/** + * Get the player from the given player id + * + * @param playerId the id of the player + * @param searchType the type of search to perform + * @returns the player + */ +async function fetchPlayerData( + playerId: string, +): Promise { + const response = await fetchQueue.fetch( + formatString(PLAYER_URL, true, playerId), + ); + const json = await response.json(); + + // Check if there was an error fetching the user data + console.log(json); + + return json as BeatLeaderPlayer; +} + +/** + * 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 = { + fetchPlayerData, + fetchScores, + fetchAllScores, +};