feat(overlay): add leaderboard toggle for BL and SS
All checks were successful
deploy / deploy (push) Successful in 58s

This commit is contained in:
Lee 2023-11-05 23:06:28 +00:00
parent fb2b72875f
commit 3e5f141938
23 changed files with 424 additions and 18 deletions

@ -43,6 +43,12 @@ const nextConfig = {
port: "", port: "",
pathname: "/**", pathname: "/**",
}, },
{
protocol: "https",
hostname: "avatars.akamai.steamstatic.com",
port: "",
pathname: "/**",
},
], ],
}, },
}; };

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 KiB

@ -79,10 +79,10 @@ export default function Analytics() {
id: "scoresaber", id: "scoresaber",
value: "ScoreSaber", value: "ScoreSaber",
}, },
// { {
// id: "beatleader", id: "beatleader",
// value: "BeatLeader", value: "BeatLeader",
// }, },
]} ]}
onChange={(value) => { onChange={(value) => {
settingsStore.setPlatform(value); settingsStore.setPlatform(value);

@ -7,7 +7,8 @@ import ScoreStats from "@/components/overlay/ScoreStats";
import SongInfo from "@/components/overlay/SongInfo"; import SongInfo from "@/components/overlay/SongInfo";
import { Card, CardDescription, CardTitle } from "@/components/ui/card"; import { Card, CardDescription, CardTitle } from "@/components/ui/card";
import { HttpSiraStatus } from "@/overlay/httpSiraStatus"; 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 { ScoreSaberAPI } from "@/utils/scoresaber/api";
import { Component } from "react"; import { Component } from "react";
@ -17,7 +18,7 @@ interface OverlayProps {}
interface OverlayState { interface OverlayState {
mounted: boolean; mounted: boolean;
player: ScoresaberPlayer | undefined; player: OverlayPlayer | undefined;
settings: any | undefined; settings: any | undefined;
} }
@ -31,13 +32,44 @@ export default class Overlay extends Component<OverlayProps, OverlayState> {
}; };
} }
updatePlayer = async (playerId: string) => { updatePlayer = async (
playerId: string,
leaderboard: "scoresaber" | "beatleader" = "scoresaber",
) => {
console.log(`Updating player stats for ${playerId}`); console.log(`Updating player stats for ${playerId}`);
const player = await ScoreSaberAPI.fetchPlayerData(playerId); if (leaderboard == "scoresaber") {
if (!player) { const player = await ScoreSaberAPI.fetchPlayerData(playerId);
return; 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() { componentDidMount() {
@ -60,9 +92,9 @@ export default class Overlay extends Component<OverlayProps, OverlayState> {
this.setState({ settings: settings }); this.setState({ settings: settings });
if (settings.settings.showPlayerStats) { if (settings.settings.showPlayerStats) {
this.updatePlayer(settings.accountId); this.updatePlayer(settings.accountId, settings.platform);
setInterval(() => { setInterval(() => {
this.updatePlayer(settings.accountId); this.updatePlayer(settings.accountId, settings.platform);
}, UPDATE_INTERVAL); }, UPDATE_INTERVAL);
} }
} }
@ -113,7 +145,7 @@ export default class Overlay extends Component<OverlayProps, OverlayState> {
<main> <main>
<div> <div>
{this.state.settings.settings.showPlayerStats && player && ( {this.state.settings.settings.showPlayerStats && player && (
<PlayerStats player={player} /> <PlayerStats player={player} settings={this.state.settings} />
)} )}
{this.state.settings.settings.showScoreStats && <ScoreStats />} {this.state.settings.settings.showScoreStats && <ScoreStats />}
</div> </div>

@ -1,14 +1,20 @@
import { ScoresaberPlayer } from "@/schemas/scoresaber/player"; import { OverlayPlayer } from "@/overlay/type/overlayPlayer";
import { formatNumber } from "@/utils/numberUtils"; import { formatNumber } from "@/utils/numberUtils";
import { GlobeAltIcon } from "@heroicons/react/20/solid"; import { GlobeAltIcon } from "@heroicons/react/20/solid";
import Image from "next/image"; import Image from "next/image";
import CountyFlag from "../CountryFlag"; import CountyFlag from "../CountryFlag";
type PlayerStatsProps = { type PlayerStatsProps = {
player: ScoresaberPlayer; player: OverlayPlayer;
settings: any;
}; };
export default function PlayerStats({ player }: PlayerStatsProps) { const leaderboardImages: Record<string, string> = {
scoresaber: "/assets/logos/scoresaber.png",
beatleader: "/assets/logos/beatleader.png",
};
export default function PlayerStats({ player, settings }: PlayerStatsProps) {
return ( return (
<div className="flex gap-2 p-2"> <div className="flex gap-2 p-2">
<Image <Image
@ -19,7 +25,15 @@ export default function PlayerStats({ player }: PlayerStatsProps) {
height={180} height={180}
/> />
<div> <div>
<p className="text-3xl font-bold">{formatNumber(player.pp, 2)}pp</p> <div className="flex gap-2">
<Image
alt="Leaderboard logo"
src={leaderboardImages[settings.platform]}
width={36}
height={36}
/>
<p className="text-3xl font-bold">{formatNumber(player.pp, 2)}pp</p>
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<GlobeAltIcon width={25} height={25} /> <GlobeAltIcon width={25} height={25} />
<p className="text-3xl">#{formatNumber(player.rank)}</p> <p className="text-3xl">#{formatNumber(player.rank)}</p>

@ -0,0 +1,8 @@
export type OverlayPlayer = {
id: string;
country: string;
profilePicture: string;
pp: number;
rank: number;
countryRank: number;
};

@ -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;
};

@ -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;
};

@ -0,0 +1,5 @@
export type BeatleaderMetadata = {
itemsPerPage: number;
page: number;
total: number;
};

@ -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;
};

@ -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;
};

@ -0,0 +1,9 @@
export type BeatLeaderPlayer = {
id: string;
country: string;
avatar: string;
pp: number;
rank: number;
countryRank: number;
// todo: finish this
};

@ -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;
};

@ -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;
};

@ -0,0 +1,8 @@
export type BeatleaderScoreOffsets = {
id: number;
frames: number;
notes: number;
walls: number;
heights: number;
pauses: number;
};

@ -0,0 +1,5 @@
import { BeatleaderSmallerSong } from "./smallerSong";
export type BeatleaderSmallerLeaderboard = {
song: BeatleaderSmallerSong;
};

@ -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;
};

@ -0,0 +1,9 @@
export type BeatleaderSmallerScoreImprovement = {
score: number;
accuracy: number;
accRight: number;
accLeft: number;
badCuts: number;
missedNotes: number;
bombCuts: number;
};

@ -0,0 +1,4 @@
export type BeatleaderSmallerSong = {
hash: string;
bpm: number;
};

@ -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; // ??
};

126
src/utils/beatleader/api.ts Normal file

@ -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<BeatLeaderPlayer | undefined> {
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<BeatleaderScore[] | undefined> {
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,
};