From fa2ba83c7a747b65e46204ec755ba74634153d1c Mon Sep 17 00:00:00 2001 From: Liam Date: Tue, 22 Oct 2024 15:59:41 +0100 Subject: [PATCH] add beatleader data tracking!!!!!!!!!!!!! --- bun.lockb | Bin 363336 -> 363848 bytes package.json | 3 +- projects/backend/src/bot/bot.ts | 3 +- projects/backend/src/common/cache.util.ts | 5 + projects/backend/src/index.ts | 23 ++-- .../backend/src/service/player.service.ts | 41 ------ projects/backend/src/service/score.service.ts | 120 +++++++++++++++++- .../impl/scoresaber-leaderboard.ts | 4 +- .../common/src/model/additional-score-data.ts | 95 ++++++++++++++ projects/common/src/player/player-history.ts | 4 +- projects/common/src/score/score.ts | 6 + .../beatleader/beatleader-difficulty-token.ts | 30 +++++ .../beatleader-leaderboard-token.ts | 16 +++ .../beatleader-modifier-rating-token.ts | 18 +++ .../beatleader/beatleader-modifiers-token.ts | 16 +++ .../beatleader/beatleader-player-token.ts | 10 ++ .../beatleader-score-improvement-token.ts | 19 +++ .../beatleader-score-offsets-token.ts | 8 ++ .../beatleader/beatleader-score-token.ts | 52 ++++++++ .../token/beatleader/beatleader-song-token.ts | 16 +++ .../src/websocket/beatleader-websocket.ts | 30 +++++ .../src/websocket/scoresaber-websocket.ts | 84 ++++-------- projects/common/src/websocket/websocket.ts | 89 +++++++++++++ projects/website/package.json | 1 + .../leaderboard/leaderboard-data.tsx | 19 +-- .../leaderboard/leaderboard-scores.tsx | 9 +- .../skeleton/leaderboard-score-skeleton.tsx | 47 +++++++ .../skeleton/leaderboard-scores-skeleton.tsx | 39 ++++++ .../src/components/player/player-info.tsx | 42 ++++-- .../website/src/components/ranking/mini.tsx | 11 +- .../ranking/player-ranking-skeleton.tsx | 2 +- .../components/score/badges/score-misses.tsx | 6 + .../src/components/score/score-modifiers.tsx | 10 +- .../src/components/score/score-stats.tsx | 38 +++++- .../website/src/components/score/score.tsx | 20 ++- projects/website/tailwind.config.ts | 4 + 36 files changed, 767 insertions(+), 173 deletions(-) create mode 100644 projects/common/src/model/additional-score-data.ts create mode 100644 projects/common/src/types/token/beatleader/beatleader-difficulty-token.ts create mode 100644 projects/common/src/types/token/beatleader/beatleader-leaderboard-token.ts create mode 100644 projects/common/src/types/token/beatleader/beatleader-modifier-rating-token.ts create mode 100644 projects/common/src/types/token/beatleader/beatleader-modifiers-token.ts create mode 100644 projects/common/src/types/token/beatleader/beatleader-player-token.ts create mode 100644 projects/common/src/types/token/beatleader/beatleader-score-improvement-token.ts create mode 100644 projects/common/src/types/token/beatleader/beatleader-score-offsets-token.ts create mode 100644 projects/common/src/types/token/beatleader/beatleader-score-token.ts create mode 100644 projects/common/src/types/token/beatleader/beatleader-song-token.ts create mode 100644 projects/common/src/websocket/beatleader-websocket.ts create mode 100644 projects/common/src/websocket/websocket.ts create mode 100644 projects/website/src/components/leaderboard/skeleton/leaderboard-score-skeleton.tsx create mode 100644 projects/website/src/components/leaderboard/skeleton/leaderboard-scores-skeleton.tsx diff --git a/bun.lockb b/bun.lockb index 744826b9606720dc14dd612c7fae4c05b27cfbe3..4b8e43091344b9c0f3b34153b7b131c50f3bc9e4 100755 GIT binary patch delta 74104 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 delta 74461 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 M_ay2x#q{a_0k>BgHvj+t diff --git a/package.json b/package.json index 37bd0da..9027e4e 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "author": "fascinated7", "license": "MIT", "dependencies": { - "concurrently": "^9.0.1" + "concurrently": "^9.0.1", + "cross-env": "^7.0.3" } } diff --git a/projects/backend/src/bot/bot.ts b/projects/backend/src/bot/bot.ts index 89f1a86..e97ef15 100644 --- a/projects/backend/src/bot/bot.ts +++ b/projects/backend/src/bot/bot.ts @@ -1,6 +1,6 @@ import { Client, MetadataStorage } from "discordx"; -import { Config } from "@ssr/common/config"; import { ActivityType, EmbedBuilder } from "discord.js"; +import { Config } from "@ssr/common/config"; export enum DiscordChannels { trackedPlayerLogs = "1295985197262569512", @@ -12,6 +12,7 @@ const DiscordBot = new Client({ intents: [], presence: { status: "online", + activities: [ { name: "scores...", diff --git a/projects/backend/src/common/cache.util.ts b/projects/backend/src/common/cache.util.ts index a59428a..5e98303 100644 --- a/projects/backend/src/common/cache.util.ts +++ b/projects/backend/src/common/cache.util.ts @@ -1,5 +1,6 @@ import { SSRCache } from "@ssr/common/cache"; import { InternalServerError } from "../error/internal-server-error"; +import { isProduction } from "@ssr/common/utils/utils"; /** * Fetches data with caching. @@ -13,6 +14,10 @@ export async function fetchWithCache( cacheKey: string, fetchFn: () => Promise ): Promise { + if (!isProduction()) { + return await fetchFn(); + } + if (cache == undefined) { throw new InternalServerError(`Cache is not defined`); } diff --git a/projects/backend/src/index.ts b/projects/backend/src/index.ts index d1c0832..0afb4f1 100644 --- a/projects/backend/src/index.ts +++ b/projects/backend/src/index.ts @@ -14,16 +14,16 @@ import { PlayerService } from "./service/player.service"; import { cron } from "@elysiajs/cron"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { delay, isProduction } from "@ssr/common/utils/utils"; -import { connectScoreSaberWebSocket } from "@ssr/common/websocket/scoresaber-websocket"; import ImageController from "./controller/image.controller"; import { ScoreService } from "./service/score.service"; import { Config } from "@ssr/common/config"; import { PlayerDocument, PlayerModel } from "@ssr/common/model/player"; import ScoresController from "./controller/scores.controller"; import LeaderboardController from "./controller/leaderboard.controller"; -import { DiscordChannels, initDiscordBot, logToChannel } from "./bot/bot"; -import { EmbedBuilder } from "discord.js"; import { getAppVersion } from "./common/app.util"; +import { connectScoresaberWebsocket } from "@ssr/common/websocket/scoresaber-websocket"; +import { connectBeatLeaderWebsocket } from "@ssr/common/websocket/beatleader-websocket"; +import { initDiscordBot } from "./bot/bot"; // Load .env file dotenv.config({ @@ -35,16 +35,15 @@ dotenv.config({ await mongoose.connect(Config.mongoUri!); // Connect to MongoDB setLogLevel("DEBUG"); -connectScoreSaberWebSocket({ - onScore: async playerScore => { - await PlayerService.trackScore(playerScore); - await ScoreService.notifyNumberOne(playerScore); +connectScoresaberWebsocket({ + onScore: async score => { + await ScoreService.trackScoreSaberScore(score); + await ScoreService.notifyNumberOne(score); }, - onDisconnect: async error => { - await logToChannel( - DiscordChannels.backendLogs, - new EmbedBuilder().setDescription(`ScoreSaber websocket disconnected: ${error}`) - ); +}); +connectBeatLeaderWebsocket({ + onScore: async score => { + await ScoreService.trackBeatLeaderScore(score); }, }); diff --git a/projects/backend/src/service/player.service.ts b/projects/backend/src/service/player.service.ts index dd6ed6d..db12917 100644 --- a/projects/backend/src/service/player.service.ts +++ b/projects/backend/src/service/player.service.ts @@ -4,7 +4,6 @@ import { getDaysAgoDate, getMidnightAlignedDate } from "@ssr/common/utils/time-u 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"; import { formatPp } from "@ssr/common/utils/number-utils"; import { getPageFromRank, isProduction } from "@ssr/common/utils/utils"; import { DiscordChannels, logToChannel } from "../bot/bot"; @@ -171,46 +170,6 @@ export class PlayerService { console.log(`Tracked player "${foundPlayer.id}"!`); } - /** - * Track player score. - * - * @param score the score to track - * @param leaderboard the leaderboard to track - */ - public static async trackScore({ score, leaderboard }: ScoreSaberPlayerScoreToken) { - const playerId = score.leaderboardPlayerInfo.id; - const playerName = score.leaderboardPlayerInfo.name; - const player: PlayerDocument | null = await PlayerModel.findById(playerId); - // Player is not tracked, so ignore the score. - if (player == undefined) { - return; - } - - const today = new Date(); - const history = player.getHistoryByDate(today); - const scores = history.scores || { - rankedScores: 0, - unrankedScores: 0, - }; - if (leaderboard.stars > 0) { - scores.rankedScores!++; - } else { - scores.unrankedScores!++; - } - - history.scores = scores; - player.setStatisticHistory(today, history); - player.sortStatisticHistory(); - - // Save the changes - player.markModified("statisticHistory"); - await player.save(); - - console.log( - `Updated scores set statistic for "${playerName}"(${playerId}), scores today: ${scores.rankedScores} ranked, ${scores.unrankedScores} unranked` - ); - } - /** * Gets the players around a player. * diff --git a/projects/backend/src/service/score.service.ts b/projects/backend/src/service/score.service.ts index 4c12b97..8717f26 100644 --- a/projects/backend/src/service/score.service.ts +++ b/projects/backend/src/service/score.service.ts @@ -23,6 +23,9 @@ import { EmbedBuilder } from "discord.js"; import { Config } from "@ssr/common/config"; import { SSRCache } from "@ssr/common/cache"; import { fetchWithCache } from "../common/cache.util"; +import { PlayerDocument, PlayerModel } from "@ssr/common/model/player"; +import { BeatLeaderScoreToken } from "@ssr/common/types/token/beatleader/beatleader-score-token"; +import { AdditionalScoreData, AdditionalScoreDataModel } from "@ssr/common/model/additional-score-data"; const playerScoresCache = new SSRCache({ ttl: 1000 * 60, // 1 minute @@ -111,6 +114,107 @@ export class ScoreService { ); } + /** + * Tracks ScoreSaber score. + * + * @param score the score to track + * @param leaderboard the leaderboard to track + */ + public static async trackScoreSaberScore({ score, leaderboard }: ScoreSaberPlayerScoreToken) { + const playerId = score.leaderboardPlayerInfo.id; + const playerName = score.leaderboardPlayerInfo.name; + const player: PlayerDocument | null = await PlayerModel.findById(playerId); + // Player is not tracked, so ignore the score. + if (player == undefined) { + return; + } + + const today = new Date(); + const history = player.getHistoryByDate(today); + const scores = history.scores || { + rankedScores: 0, + unrankedScores: 0, + }; + if (leaderboard.stars > 0) { + scores.rankedScores!++; + } else { + scores.unrankedScores!++; + } + + history.scores = scores; + player.setStatisticHistory(today, history); + player.sortStatisticHistory(); + + // Save the changes + player.markModified("statisticHistory"); + await player.save(); + + console.log( + `Updated scores set statistic for "${playerName}"(${playerId}), scores today: ${scores.rankedScores} ranked, ${scores.unrankedScores} unranked` + ); + } + + /** + * Tracks BeatLeader score. + * + * @param score the score to track + */ + public static async trackBeatLeaderScore(score: BeatLeaderScoreToken) { + const { playerId, player: scorePlayer, leaderboard } = score; + const player: PlayerDocument | null = await PlayerModel.findById(playerId); + // Player is not tracked, so ignore the score. + if (player == undefined) { + return; + } + + const difficulty = leaderboard.difficulty; + const difficultyKey = `${difficulty.difficultyName.replace("Plus", "+")}-${difficulty.modeName}`; + await AdditionalScoreDataModel.create({ + playerId: playerId, + songHash: leaderboard.song.hash, + songDifficulty: difficultyKey, + songScore: score.baseScore, + bombCuts: score.bombCuts, + wallsHit: score.wallsHit, + pauses: score.pauses, + fcAccuracy: score.fcAccuracy * 100, + handAccuracy: { + left: score.accLeft, + right: score.accRight, + }, + } as AdditionalScoreData); + console.log( + `Tracked additional score data for "${scorePlayer.name}"(${playerId}), difficulty: ${difficultyKey}, score: ${score.baseScore}` + ); + } + + /** + * Gets the additional score data for a player's score. + * + * @param playerId the id of the player + * @param songHash the hash of the map + * @param songDifficulty the difficulty of the map + * @param songScore the score of the play + * @private + */ + private static async getAdditionalScoreData( + playerId: string, + songHash: string, + songDifficulty: string, + songScore: number + ): Promise { + const additionalData = await AdditionalScoreDataModel.findOne({ + playerId: playerId, + songHash: songHash, + songDifficulty: songDifficulty, + songScore: songScore, + }); + if (!additionalData) { + return undefined; + } + return additionalData.toObject(); + } + /** * Gets scores for a player. * @@ -128,12 +232,12 @@ export class ScoreService { sort: string, search?: string ): Promise | undefined> { + console.log("hi"); return fetchWithCache( playerScoresCache, `player-scores-${leaderboardName}-${id}-${page}-${sort}-${search}`, async () => { const scores: PlayerScore[] | undefined = []; - let beatSaverMap: BeatSaverMap | undefined; let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values switch (leaderboardName) { @@ -164,12 +268,22 @@ export class ScoreService { if (tokenLeaderboard == undefined) { continue; } - beatSaverMap = await BeatSaverService.getMap(tokenLeaderboard.songHash); + + const additionalData = await this.getAdditionalScoreData( + id, + tokenLeaderboard.songHash, + `${tokenLeaderboard.difficulty.difficulty}-${tokenLeaderboard.difficulty.gameMode}`, + score.score + ); + console.log("additionalData", additionalData); + if (additionalData !== undefined) { + score.additionalData = additionalData; + } scores.push({ score: score, leaderboard: tokenLeaderboard, - beatSaver: beatSaverMap, + beatSaver: await BeatSaverService.getMap(tokenLeaderboard.songHash), }); } break; diff --git a/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts b/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts index 29b7805..df8ef39 100644 --- a/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts +++ b/projects/common/src/leaderboard/impl/scoresaber-leaderboard.ts @@ -41,7 +41,7 @@ export function getScoreSaberLeaderboardFromToken(token: ScoreSaberLeaderboardTo const difficulty: LeaderboardDifficulty = { leaderboardId: token.difficulty.leaderboardId, difficulty: getDifficultyFromScoreSaberDifficulty(token.difficulty.difficulty), - gameMode: token.difficulty.gameMode, + gameMode: token.difficulty.gameMode.replace("Solo", ""), difficultyRaw: token.difficulty.difficultyRaw, }; @@ -66,7 +66,7 @@ export function getScoreSaberLeaderboardFromToken(token: ScoreSaberLeaderboardTo return { leaderboardId: difficulty.leaderboardId, difficulty: getDifficultyFromScoreSaberDifficulty(difficulty.difficulty), - gameMode: difficulty.gameMode, + gameMode: difficulty.gameMode.replace("Solo", ""), difficultyRaw: difficulty.difficultyRaw, }; }) diff --git a/projects/common/src/model/additional-score-data.ts b/projects/common/src/model/additional-score-data.ts new file mode 100644 index 0000000..1db676d --- /dev/null +++ b/projects/common/src/model/additional-score-data.ts @@ -0,0 +1,95 @@ +import { getModelForClass, modelOptions, prop, ReturnModelType, Severity } from "@typegoose/typegoose"; +import { Document } from "mongoose"; + +/** + * The model for a BeatSaver map. + */ +@modelOptions({ + options: { allowMixed: Severity.ALLOW }, + schemaOptions: { + collection: "additional-score-data", + toObject: { + virtuals: true, + transform: function (_, ret) { + delete ret._id; + delete ret.playerId; + delete ret.songHash; + delete ret.songDifficulty; + delete ret.songScore; + delete ret.__v; + return ret; + }, + }, + }, +}) +export class AdditionalScoreData { + /** + * The of the player who set the score. + */ + @prop({ required: true, index: true }) + public playerId!: string; + + /** + * The hash of the song. + */ + @prop({ required: true, index: true }) + public songHash!: string; + + /** + * The difficulty the score was set on. + */ + @prop({ required: true, index: true }) + public songDifficulty!: string; + + /** + * The score of the play. + */ + @prop({ required: true, index: true }) + public songScore!: number; + + /** + * The amount of times a bomb was hit. + */ + + @prop({ required: false }) + public bombCuts!: number; + + /** + * The amount of walls hit in the play. + */ + @prop({ required: false }) + public wallsHit!: number; + + /** + * The amount of pauses in the play. + */ + @prop({ required: false }) + public pauses!: number; + + /** + * The hand accuracy for each hand. + * @private + */ + @prop({ required: false }) + public handAccuracy!: { + /** + * The left hand accuracy. + */ + left: number; + + /** + * The right hand accuracy. + */ + right: number; + }; + + /** + * The full combo accuracy of the play. + */ + @prop({ required: true }) + public fcAccuracy!: number; +} + +export type AdditionalScoreDataDocument = AdditionalScoreData & Document; +export const AdditionalScoreDataModel: ReturnModelType = + getModelForClass(AdditionalScoreData); diff --git a/projects/common/src/player/player-history.ts b/projects/common/src/player/player-history.ts index d9f4505..b9fbbc3 100644 --- a/projects/common/src/player/player-history.ts +++ b/projects/common/src/player/player-history.ts @@ -35,7 +35,7 @@ export interface PlayerHistory { }; /** - * The amount of scores set for this day. + * The player's scores stats. */ scores?: { /** @@ -60,7 +60,7 @@ export interface PlayerHistory { }; /** - * The player's accuracy. + * The player's accuracy stats. */ accuracy?: { /** diff --git a/projects/common/src/score/score.ts b/projects/common/src/score/score.ts index f56c09a..2426d8e 100644 --- a/projects/common/src/score/score.ts +++ b/projects/common/src/score/score.ts @@ -1,5 +1,6 @@ import { Modifier } from "./modifier"; import { Leaderboards } from "../leaderboard"; +import { AdditionalScoreData } from "../model/additional-score-data"; export default interface Score { /** @@ -53,6 +54,11 @@ export default interface Score { */ readonly fullCombo: boolean; + /** + * The additional data for the score. + */ + additionalData?: AdditionalScoreData; + /** * The time the score was set. * @private diff --git a/projects/common/src/types/token/beatleader/beatleader-difficulty-token.ts b/projects/common/src/types/token/beatleader/beatleader-difficulty-token.ts new file mode 100644 index 0000000..ab21362 --- /dev/null +++ b/projects/common/src/types/token/beatleader/beatleader-difficulty-token.ts @@ -0,0 +1,30 @@ +import { BeatLeaderModifierToken } from "./beatleader-modifiers-token"; +import { BeatLeaderModifierRatingToken } from "./beatleader-modifier-rating-token"; + +export type BeatLeaderDifficultyToken = { + id: number; + value: number; + mode: number; + difficultyName: string; + modeName: string; + status: number; + modifierValues: BeatLeaderModifierToken; + modifiersRating: BeatLeaderModifierRatingToken; + 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/projects/common/src/types/token/beatleader/beatleader-leaderboard-token.ts b/projects/common/src/types/token/beatleader/beatleader-leaderboard-token.ts new file mode 100644 index 0000000..6cd979a --- /dev/null +++ b/projects/common/src/types/token/beatleader/beatleader-leaderboard-token.ts @@ -0,0 +1,16 @@ +import { BeatLeaderSongToken } from "./beatleader-song-token"; +import { BeatLeaderDifficultyToken } from "./beatleader-difficulty-token"; + +export type BeatLeaderLeaderboardToken = { + id: string; + song: BeatLeaderSongToken; + difficulty: BeatLeaderDifficultyToken; + scores: null; // ?? + changes: null; // ?? + qualification: null; // ?? + reweight: null; // ?? + leaderboardGroup: null; // ?? + plays: number; + clan: null; // ?? + clanRankingContested: boolean; +}; diff --git a/projects/common/src/types/token/beatleader/beatleader-modifier-rating-token.ts b/projects/common/src/types/token/beatleader/beatleader-modifier-rating-token.ts new file mode 100644 index 0000000..5e0b549 --- /dev/null +++ b/projects/common/src/types/token/beatleader/beatleader-modifier-rating-token.ts @@ -0,0 +1,18 @@ +export type BeatLeaderModifierRatingToken = { + 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/projects/common/src/types/token/beatleader/beatleader-modifiers-token.ts b/projects/common/src/types/token/beatleader/beatleader-modifiers-token.ts new file mode 100644 index 0000000..e4b0e34 --- /dev/null +++ b/projects/common/src/types/token/beatleader/beatleader-modifiers-token.ts @@ -0,0 +1,16 @@ +export type BeatLeaderModifierToken = { + 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/projects/common/src/types/token/beatleader/beatleader-player-token.ts b/projects/common/src/types/token/beatleader/beatleader-player-token.ts new file mode 100644 index 0000000..e28b811 --- /dev/null +++ b/projects/common/src/types/token/beatleader/beatleader-player-token.ts @@ -0,0 +1,10 @@ +export type BeatLeaderPlayerToken = { + id: string; + country: string; + avatar: string; + pp: number; + rank: number; + countryRank: number; + name: string; + // todo: finish this +}; diff --git a/projects/common/src/types/token/beatleader/beatleader-score-improvement-token.ts b/projects/common/src/types/token/beatleader/beatleader-score-improvement-token.ts new file mode 100644 index 0000000..0740e34 --- /dev/null +++ b/projects/common/src/types/token/beatleader/beatleader-score-improvement-token.ts @@ -0,0 +1,19 @@ +export type BeatLeaderScoreImprovementToken = { + 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/projects/common/src/types/token/beatleader/beatleader-score-offsets-token.ts b/projects/common/src/types/token/beatleader/beatleader-score-offsets-token.ts new file mode 100644 index 0000000..1d0b1c9 --- /dev/null +++ b/projects/common/src/types/token/beatleader/beatleader-score-offsets-token.ts @@ -0,0 +1,8 @@ +export type BeatLeaderScoreOffsetsToken = { + id: number; + frames: number; + notes: number; + walls: number; + heights: number; + pauses: number; +}; diff --git a/projects/common/src/types/token/beatleader/beatleader-score-token.ts b/projects/common/src/types/token/beatleader/beatleader-score-token.ts new file mode 100644 index 0000000..cf0bf0b --- /dev/null +++ b/projects/common/src/types/token/beatleader/beatleader-score-token.ts @@ -0,0 +1,52 @@ +import { BeatLeaderLeaderboardToken } from "./beatleader-leaderboard-token"; +import { BeatLeaderScoreImprovementToken } from "./beatleader-score-improvement-token"; +import { BeatLeaderScoreOffsetsToken } from "./beatleader-score-offsets-token"; +import { BeatLeaderPlayerToken } from "./beatleader-player-token"; + +export type BeatLeaderScoreToken = { + myScore: null; // ?? + validContexts: number; + leaderboard: BeatLeaderLeaderboardToken; + 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: BeatLeaderPlayerToken; // ?? + scoreImprovement: BeatLeaderScoreImprovementToken; + rankVoting: null; // ?? + metadata: null; // ?? + offsets: BeatLeaderScoreOffsetsToken; +}; diff --git a/projects/common/src/types/token/beatleader/beatleader-song-token.ts b/projects/common/src/types/token/beatleader/beatleader-song-token.ts new file mode 100644 index 0000000..cc92839 --- /dev/null +++ b/projects/common/src/types/token/beatleader/beatleader-song-token.ts @@ -0,0 +1,16 @@ +export type BeatLeaderSongToken = { + 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/projects/common/src/websocket/beatleader-websocket.ts b/projects/common/src/websocket/beatleader-websocket.ts new file mode 100644 index 0000000..208042e --- /dev/null +++ b/projects/common/src/websocket/beatleader-websocket.ts @@ -0,0 +1,30 @@ +import { connectWebSocket, WebsocketCallbacks } from "./websocket"; +import { BeatLeaderScoreToken } from "../types/token/beatleader/beatleader-score-token"; + +type BeatLeaderWebsocket = { + /** + * Invoked when a score message is received. + * + * @param score the received score data. + */ + onScore?: (score: BeatLeaderScoreToken) => void; +} & WebsocketCallbacks; + +/** + * Connects to the BeatLeader websocket and handles incoming messages. + * + * @param onMessage the onMessage callback + * @param onScore the onScore callback + * @param onDisconnect the onDisconnect callback + */ +export function connectBeatLeaderWebsocket({ onMessage, onScore, onDisconnect }: BeatLeaderWebsocket) { + return connectWebSocket({ + name: "BeatLeader", + url: "wss://sockets.api.beatleader.xyz/scores", + onMessage: (message: unknown) => { + onScore && onScore(message as BeatLeaderScoreToken); + onMessage && onMessage(message); + }, + onDisconnect, + }); +} diff --git a/projects/common/src/websocket/scoresaber-websocket.ts b/projects/common/src/websocket/scoresaber-websocket.ts index 636ead4..7b53411 100644 --- a/projects/common/src/websocket/scoresaber-websocket.ts +++ b/projects/common/src/websocket/scoresaber-websocket.ts @@ -1,74 +1,38 @@ -import WebSocket from "ws"; +import { connectWebSocket, WebsocketCallbacks } from "./websocket"; import ScoreSaberPlayerScoreToken from "../types/token/scoresaber/score-saber-player-score-token"; +import { ScoreSaberWebsocketMessageToken } from "../types/token/scoresaber/websocket/scoresaber-websocket-message"; -type ScoresaberSocket = { - /** - * Invoked when a general message is received. - * - * @param message the received message. - */ - onMessage?: (message: unknown) => void; - +type ScoresaberWebsocket = { /** * Invoked when a score message is received. * * @param score the received score data. */ onScore?: (score: ScoreSaberPlayerScoreToken) => void; - - /** - * Invoked when the connection is closed. - * - * @param error the error that caused the connection to close - */ - onDisconnect?: (error?: WebSocket.ErrorEvent | WebSocket.CloseEvent) => void; -}; +} & WebsocketCallbacks; /** - * Connects to the ScoreSaber websocket and handles incoming messages. + * Connects to the Scoresaber websocket and handles incoming messages. + * + * @param onMessage the onMessage callback + * @param onScore the onScore callback + * @param onDisconnect the onDisconnect callback */ -export function connectScoreSaberWebSocket({ onMessage, onScore, onDisconnect }: ScoresaberSocket) { - let websocket: WebSocket | null = null; - - function connectWs() { - websocket = new WebSocket("wss://scoresaber.com/ws"); - - websocket.onopen = () => { - console.log("Connected to the ScoreSaber WebSocket!"); - }; - - websocket.onerror = error => { - console.error("WebSocket Error:", error); - if (websocket) { - websocket.close(); // Close the connection on error +export function connectScoresaberWebsocket({ onMessage, onScore, onDisconnect }: ScoresaberWebsocket) { + return connectWebSocket({ + name: "Scoresaber", + url: "wss://scoresaber.com/ws", + onMessage: (message: unknown) => { + const command = message as ScoreSaberWebsocketMessageToken; + if (typeof command !== "object" || command === null) { + return; } - - onDisconnect && onDisconnect(error); - }; - - websocket.onclose = event => { - console.log("Lost connection to the ScoreSaber WebSocket. Attempting to reconnect..."); - - onDisconnect && onDisconnect(event); - setTimeout(connectWs, 5000); // Reconnect after 5 seconds - }; - - websocket.onmessage = messageEvent => { - if (typeof messageEvent.data !== "string") return; - - try { - const command = JSON.parse(messageEvent.data); - - if (command.commandName === "score") { - onScore && onScore(command.commandData as ScoreSaberPlayerScoreToken); - } else { - onMessage && onMessage(command); - } - } catch (err) { - console.warn("Received invalid message:", messageEvent.data); + if (command.commandName === "score") { + onScore && onScore(command.commandData as ScoreSaberPlayerScoreToken); + } else { + onMessage && onMessage(command); } - }; - } - - connectWs(); // Initiate the first connection + }, + onDisconnect, + }); } diff --git a/projects/common/src/websocket/websocket.ts b/projects/common/src/websocket/websocket.ts new file mode 100644 index 0000000..0354b50 --- /dev/null +++ b/projects/common/src/websocket/websocket.ts @@ -0,0 +1,89 @@ +import WebSocket from "ws"; +import { DiscordChannels, logToChannel } from "backend/src/bot/bot"; +import { EmbedBuilder } from "discord.js"; + +export type WebsocketCallbacks = { + /** + * Invoked when a general message is received. + * + * @param message the received message. + */ + onMessage?: (message: unknown) => void; + + /** + * Invoked when the connection is closed. + * + * @param error the error that caused the connection to close + */ + onDisconnect?: (error?: WebSocket.ErrorEvent | WebSocket.CloseEvent) => void; +}; + +type Websocket = { + /** + * The name of the service we're connecting to. + */ + name: string; + + /** + * The url of the service we're connecting to. + */ + url: string; +} & WebsocketCallbacks; + +/** + * Connects to the ScoreSaber websocket and handles incoming messages. + */ +export function connectWebSocket({ name, url, onMessage, onDisconnect }: Websocket) { + let websocket: WebSocket | null = null; + + /** + * Logs to the backend logs channel. + * + * @param error the error to log + */ + async function log(error: WebSocket.ErrorEvent | WebSocket.CloseEvent) { + await logToChannel( + DiscordChannels.backendLogs, + new EmbedBuilder().setDescription(`${name} websocket disconnected: ${JSON.stringify(error)}`) + ); + } + + function connectWs() { + websocket = new WebSocket(url); + + websocket.onopen = () => { + console.log(`Connected to the ${name} WebSocket!`); + }; + + websocket.onerror = event => { + console.error("WebSocket Error:", event); + if (websocket) { + websocket.close(); // Close the connection on error + } + + onDisconnect && onDisconnect(event); + log(event); + }; + + websocket.onclose = event => { + console.log(`Lost connection to the ${name} WebSocket. Attempting to reconnect...`); + + onDisconnect && onDisconnect(event); + log(event); + setTimeout(connectWs, 5000); // Reconnect after 5 seconds + }; + + websocket.onmessage = messageEvent => { + if (typeof messageEvent.data !== "string") return; + + try { + const command = JSON.parse(messageEvent.data); + onMessage && onMessage(command); + } catch (err) { + console.warn(`Received invalid json message on ${name}:`, messageEvent.data); + } + }; + } + + connectWs(); // Initiate the first connection +} diff --git a/projects/website/package.json b/projects/website/package.json index 08991db..73d8953 100644 --- a/projects/website/package.json +++ b/projects/website/package.json @@ -4,6 +4,7 @@ "private": true, "scripts": { "dev": "next dev --turbo", + "dev-debug": "cross-env NODE_OPTIONS='--inspect' next dev --turbo", "build": "next build", "start": "next start", "lint": "next lint" diff --git a/projects/website/src/components/leaderboard/leaderboard-data.tsx b/projects/website/src/components/leaderboard/leaderboard-data.tsx index 50aea50..1b04552 100644 --- a/projects/website/src/components/leaderboard/leaderboard-data.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-data.tsx @@ -10,6 +10,7 @@ import { useEffect, useState } from "react"; import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util"; import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response"; import LeaderboardPpChart from "@/components/leaderboard/leaderboard-pp-chart"; +import Card from "@/components/card"; type LeaderboardDataProps = { /** @@ -48,14 +49,16 @@ export function LeaderboardData({ initialLeaderboard, initialScores, initialPage const leaderboard = currentLeaderboard.leaderboard; return (
- setCurrentLeaderboardId(newId)} - showDifficulties - isLeaderboardPage - /> + + setCurrentLeaderboardId(newId)} + showDifficulties + isLeaderboardPage + /> +
{leaderboard.stars > 0 && } diff --git a/projects/website/src/components/leaderboard/leaderboard-scores.tsx b/projects/website/src/components/leaderboard/leaderboard-scores.tsx index 6b3a0c1..ce3da37 100644 --- a/projects/website/src/components/leaderboard/leaderboard-scores.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-scores.tsx @@ -4,12 +4,10 @@ import useWindowDimensions from "@/hooks/use-window-dimensions"; import { useQuery } from "@tanstack/react-query"; import { motion, useAnimation } from "framer-motion"; import { useCallback, useEffect, useRef, useState } from "react"; -import Card from "../card"; import Pagination from "../input/pagination"; import LeaderboardScore from "./leaderboard-score"; import { scoreAnimation } from "@/components/score/score-animation"; import { Button } from "@/components/ui/button"; -import { clsx } from "clsx"; import { getDifficultyFromRawDifficulty } from "@/common/song-utils"; import { fetchLeaderboardScores } from "@ssr/common/utils/score-utils"; import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; @@ -17,6 +15,7 @@ import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leade import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response"; import useDatabase from "@/hooks/use-database"; import { useLiveQuery } from "dexie-react-hooks"; +import LeaderboardScoresSkeleton from "@/components/leaderboard/skeleton/leaderboard-scores-skeleton"; type LeaderboardScoresProps = { initialPage?: number; @@ -126,11 +125,11 @@ export default function LeaderboardScores({ }, [selectedLeaderboardId, currentPage, disableUrlChanging]); if (currentScores === undefined) { - return undefined; + return ; } return ( - + <> {/* Where to scroll to when new scores are loaded */}
@@ -207,6 +206,6 @@ export default function LeaderboardScores({ setShouldFetch(true); }} /> - + ); } diff --git a/projects/website/src/components/leaderboard/skeleton/leaderboard-score-skeleton.tsx b/projects/website/src/components/leaderboard/skeleton/leaderboard-score-skeleton.tsx new file mode 100644 index 0000000..ca06940 --- /dev/null +++ b/projects/website/src/components/leaderboard/skeleton/leaderboard-score-skeleton.tsx @@ -0,0 +1,47 @@ +import { Skeleton } from "@/components/ui/skeleton"; + +export function LeaderboardScoreSkeleton() { + return ( + <> + {/* Skeleton for Score Rank */} + + + + + {/* Skeleton for Player Info */} + + + + + {/* Skeleton for Time Set */} + + + + + {/* Skeleton for Score */} + + + + + {/* Skeleton for Accuracy */} + + + + + {/* Skeleton for Misses */} + + + + + {/* Skeleton for PP */} + + + + + {/* Skeleton for Modifiers */} + + + + + ); +} diff --git a/projects/website/src/components/leaderboard/skeleton/leaderboard-scores-skeleton.tsx b/projects/website/src/components/leaderboard/skeleton/leaderboard-scores-skeleton.tsx new file mode 100644 index 0000000..44faf84 --- /dev/null +++ b/projects/website/src/components/leaderboard/skeleton/leaderboard-scores-skeleton.tsx @@ -0,0 +1,39 @@ +import { Skeleton } from "@/components/ui/skeleton"; +import { LeaderboardScoreSkeleton } from "@/components/leaderboard/skeleton/leaderboard-score-skeleton"; + +export default function LeaderboardScoresSkeleton() { + return ( + <> + {/* Loading Skeleton for the LeaderboardScores Table */} +
+ + + + + + + + + + + + + + + {/* Loop over to create 10 skeleton rows */} + {[...Array(10)].map((_, index) => ( + + + + ))} + +
RankPlayerTime SetScoreAccuracyMissesPPMods
+
+ + {/* Skeleton for Pagination */} +
+ +
+ + ); +} diff --git a/projects/website/src/components/player/player-info.tsx b/projects/website/src/components/player/player-info.tsx index 7c35d98..e910e9e 100644 --- a/projects/website/src/components/player/player-info.tsx +++ b/projects/website/src/components/player/player-info.tsx @@ -23,13 +23,39 @@ type TablePlayerProps = { */ hideCountryFlag?: boolean; + /** + * Whether to make the player name a link + */ + useLink?: boolean; + /** * Whether to apply hover brightness */ hoverBrightness?: boolean; }; -export function PlayerInfo({ player, highlightedPlayer, hideCountryFlag, hoverBrightness = true }: TablePlayerProps) { +export function PlayerInfo({ + player, + highlightedPlayer, + hideCountryFlag, + useLink, + hoverBrightness = true, +}: TablePlayerProps) { + const name = ( +

+ {player.name} +

+ ); + return (
@@ -39,19 +65,7 @@ export function PlayerInfo({ player, highlightedPlayer, hideCountryFlag, hoverBr /> {!hideCountryFlag && } - -

- {player.name} -

- + {useLink ? {name} : name}
); } diff --git a/projects/website/src/components/ranking/mini.tsx b/projects/website/src/components/ranking/mini.tsx index f392650..1199092 100644 --- a/projects/website/src/components/ranking/mini.tsx +++ b/projects/website/src/components/ranking/mini.tsx @@ -10,8 +10,6 @@ import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player"; import { getPlayersAroundPlayer } from "@ssr/common/utils/player-utils"; import { AroundPlayer } from "@ssr/common/types/around-player"; import { PlayerInfo } from "@/components/player/player-info"; -import useDatabase from "@/hooks/use-database"; -import { useLiveQuery } from "dexie-react-hooks"; const PLAYER_NAME_MAX_LENGTH = 18; @@ -50,9 +48,6 @@ const miniVariants: Variants = { }; export default function Mini({ type, player, shouldUpdate }: MiniProps) { - const database = useDatabase(); - const claimedPlayer = useLiveQuery(() => database.getClaimedPlayer()); - if (shouldUpdate == undefined) { shouldUpdate = true; } @@ -79,7 +74,7 @@ export default function Mini({ type, player, shouldUpdate }: MiniProps) { } return ( - +
{icon}

{type} Ranking

@@ -87,10 +82,6 @@ export default function Mini({ type, player, shouldUpdate }: MiniProps) {
{response.players.map((playerRanking, index) => { const rank = type == "Global" ? playerRanking.rank : playerRanking.countryRank; - const playerName = - playerRanking.name.length > PLAYER_NAME_MAX_LENGTH - ? playerRanking.name.substring(0, PLAYER_NAME_MAX_LENGTH) + "..." - : playerRanking.name; const ppDifference = playerRanking.pp - player.pp; return ( diff --git a/projects/website/src/components/ranking/player-ranking-skeleton.tsx b/projects/website/src/components/ranking/player-ranking-skeleton.tsx index be962dc..f4e8c6d 100644 --- a/projects/website/src/components/ranking/player-ranking-skeleton.tsx +++ b/projects/website/src/components/ranking/player-ranking-skeleton.tsx @@ -5,7 +5,7 @@ export function PlayerRankingSkeleton() { const skeletonArray = new Array(5).fill(0); return ( - +
{/* Icon Skeleton */} {/* Text Skeleton for Ranking */} diff --git a/projects/website/src/components/score/badges/score-misses.tsx b/projects/website/src/components/score/badges/score-misses.tsx index 0c0e67a..380d953 100644 --- a/projects/website/src/components/score/badges/score-misses.tsx +++ b/projects/website/src/components/score/badges/score-misses.tsx @@ -21,6 +21,12 @@ export default function ScoreMissesBadge({ score, hideXMark }: ScoreMissesBadgeP

Misses

Missed Notes: {formatNumberWithCommas(score.missedNotes)}

Bad Cuts: {formatNumberWithCommas(score.badCuts)}

+ {score.additionalData && ( + <> +

Bomb Cuts: {formatNumberWithCommas(score.additionalData.bombCuts)}

+

Wall Hits: {formatNumberWithCommas(score.additionalData.wallsHit)}

+ + )} ) : (

Full Combo

diff --git a/projects/website/src/components/score/score-modifiers.tsx b/projects/website/src/components/score/score-modifiers.tsx index 9b87d22..a22832f 100644 --- a/projects/website/src/components/score/score-modifiers.tsx +++ b/projects/website/src/components/score/score-modifiers.tsx @@ -11,9 +11,14 @@ type ScoreModifiersProps = { * The way to display the modifiers */ type: "full" | "simple"; + + /** + * Limit the number of modifiers to display + */ + limit?: number; }; -export function ScoreModifiers({ score, type }: ScoreModifiersProps) { +export function ScoreModifiers({ score, type, limit }: ScoreModifiersProps) { const modifiers = score.modifiers; if (modifiers.length === 0) { return

-

; @@ -21,13 +26,14 @@ export function ScoreModifiers({ score, type }: ScoreModifiersProps) { switch (type) { case "full": - return {modifiers.join(", ")}; + return {modifiers.slice(0, limit).join(", ")}; case "simple": return ( {Object.entries(Modifier) .filter(([_, mod]) => modifiers.includes(mod)) .map(([mod, _]) => mod) + .slice(0, limit) .join(",")} ); diff --git a/projects/website/src/components/score/score-stats.tsx b/projects/website/src/components/score/score-stats.tsx index 2d343ac..ddc3a8c 100644 --- a/projects/website/src/components/score/score-stats.tsx +++ b/projects/website/src/components/score/score-stats.tsx @@ -49,6 +49,7 @@ const badges: ScoreBadge[] = [ }, create: (score: ScoreSaberScore, leaderboard: ScoreSaberLeaderboard) => { const acc = (score.score / leaderboard.maxScore) * 100; + const fcAccuracy = score.additionalData?.fcAccuracy; const scoreBadge = getScoreBadgeFromAccuracy(acc); let accDetails = `${scoreBadge.name != "-" ? scoreBadge.name : ""}`; if (scoreBadge.max == null) { @@ -68,7 +69,8 @@ const badges: ScoreBadge[] = [

Accuracy

-

{accDetails}

+

Score: {accDetails}

+ {fcAccuracy &&

Full Combo: {fcAccuracy.toFixed(2)}%

}
{modCount > 0 && ( @@ -82,7 +84,7 @@ const badges: ScoreBadge[] = [ } >

- {acc.toFixed(2)}% {modCount > 0 && } + {acc.toFixed(2)}% {modCount > 0 && }

@@ -96,12 +98,36 @@ const badges: ScoreBadge[] = [ }, }, { - name: "", - create: () => undefined, + name: "Left Hand Accuracy", + color: () => "bg-hands-left", + create: (score: ScoreSaberScore) => { + if (!score.additionalData) { + return undefined; + } + + const { handAccuracy } = score.additionalData; + return ( + +

{handAccuracy.left.toFixed(2)}

+
+ ); + }, }, { - name: "", - create: () => undefined, + name: "Right Hand Accuracy", + color: () => "bg-hands-right", + create: (score: ScoreSaberScore) => { + if (!score.additionalData) { + return undefined; + } + + const { handAccuracy } = score.additionalData; + return ( + +

{handAccuracy.right.toFixed(2)}

+
+ ); + }, }, { name: "Full Combo", diff --git a/projects/website/src/components/score/score.tsx b/projects/website/src/components/score/score.tsx index ce2e0fc..e906e53 100644 --- a/projects/website/src/components/score/score.tsx +++ b/projects/website/src/components/score/score.tsx @@ -13,6 +13,8 @@ import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import { BeatSaverMap } from "@ssr/common/model/beatsaver/beatsaver-map"; import { useIsMobile } from "@/hooks/use-is-mobile"; +import Card from "@/components/card"; +import StatValue from "@/components/stat-value"; type Props = { /** @@ -103,11 +105,19 @@ export default function Score({ leaderboard, beatSaverMap, score, settings }: Pr animate={{ opacity: 1, y: 0 }} className="w-full mt-2" > - + + {score.additionalData && ( +
+ +
+ )} + + +
)}
diff --git a/projects/website/tailwind.config.ts b/projects/website/tailwind.config.ts index d905875..f703e27 100644 --- a/projects/website/tailwind.config.ts +++ b/projects/website/tailwind.config.ts @@ -14,6 +14,10 @@ const config: Config = { ssr: { DEFAULT: "#6773ff", }, + hands: { + left: "rgba(168,32,32,1)", + right: "rgba(32,100,168,1)", + }, background: "hsl(var(--background))", foreground: "hsl(var(--foreground))", card: {