From d42c888e82ee9cf6aebeec209a782c8a4bc560ad Mon Sep 17 00:00:00 2001 From: Liam Date: Wed, 23 Oct 2024 20:20:57 +0100 Subject: [PATCH] implement scoresaber score tracking (for previous scores) --- bun.lockb | Bin 358576 -> 359080 bytes projects/backend/package.json | 1 + projects/backend/src/index.ts | 4 +- projects/backend/src/service/score.service.ts | 126 ++++++++++-------- projects/common/package.json | 1 + .../additional-score-data.ts | 2 +- .../src/model/score/impl/scoresaber-score.ts | 123 +++++++++++++++++ projects/common/src/model/score/score.ts | 96 +++++++++++++ .../common/src/score/impl/scoresaber-score.ts | 76 ----------- projects/common/src/score/score.ts | 67 ---------- ...ore-saber-leaderboard-player-info-token.ts | 4 +- .../scoresaber/score-saber-score-token.ts | 2 +- projects/common/src/utils/scoresaber.util.ts | 2 +- .../(pages)/leaderboard/[...slug]/page.tsx | 2 +- .../src/app/(pages)/player/[...slug]/page.tsx | 2 +- .../chart/player-score-accuracy-chart.tsx | 1 - .../leaderboard/leaderboard-data.tsx | 2 +- .../leaderboard/leaderboard-score.tsx | 2 +- .../leaderboard/leaderboard-scores.tsx | 2 +- .../src/components/player/player-data.tsx | 2 +- .../src/components/player/player-scores.tsx | 2 +- .../components/score/badges/badge-props.ts | 2 +- .../src/components/score/score-badge.tsx | 2 +- .../src/components/score/score-buttons.tsx | 2 +- .../components/score/score-editor-button.tsx | 2 +- .../score/score-feed/score-feed.tsx | 2 +- .../src/components/score/score-modifiers.tsx | 2 +- .../src/components/score/score-rank-info.tsx | 2 +- .../src/components/score/score-stats.tsx | 2 +- .../src/components/score/score-time-set.tsx | 2 +- .../website/src/components/score/score.tsx | 2 +- 31 files changed, 315 insertions(+), 224 deletions(-) create mode 100644 projects/common/src/model/score/impl/scoresaber-score.ts create mode 100644 projects/common/src/model/score/score.ts delete mode 100644 projects/common/src/score/impl/scoresaber-score.ts delete mode 100644 projects/common/src/score/score.ts diff --git a/bun.lockb b/bun.lockb index af22972cb61535d920724aac285ccca7a10e97f0..7933a7944d86c590c94025c1ce1d55f6323574ef 100755 GIT binary patch delta 54325 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 delta 54374 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 R1N&w!{(KVY$P~lP{{Z#PXpjH^ diff --git a/projects/backend/package.json b/projects/backend/package.json index 4a5e990..5466fec 100644 --- a/projects/backend/package.json +++ b/projects/backend/package.json @@ -14,6 +14,7 @@ "@elysiajs/swagger": "^1.1.3", "@ssr/common": "workspace:common", "@tqman/nice-logger": "^1.0.1", + "@typegoose/auto-increment": "^4.7.0", "@typegoose/typegoose": "^12.8.0", "@typescript-eslint/eslint-plugin": "^8.9.0", "@typescript-eslint/parser": "^8.9.0", diff --git a/projects/backend/src/index.ts b/projects/backend/src/index.ts index 575d649..37d3e54 100644 --- a/projects/backend/src/index.ts +++ b/projects/backend/src/index.ts @@ -8,7 +8,6 @@ import { etag } from "@bogeychan/elysia-etag"; import AppController from "./controller/app.controller"; import * as dotenv from "@dotenvx/dotenvx"; import mongoose from "mongoose"; -import { setLogLevel } from "@typegoose/typegoose"; import PlayerController from "./controller/player.controller"; import { PlayerService } from "./service/player.service"; import { cron } from "@elysiajs/cron"; @@ -33,9 +32,10 @@ dotenv.config({ override: true, }); +// Connect to Mongo await mongoose.connect(Config.mongoUri!); // Connect to MongoDB -setLogLevel("DEBUG"); +// Connect to websockets connectScoresaberWebsocket({ onScore: async score => { await ScoreService.trackScoreSaberScore(score); diff --git a/projects/backend/src/service/score.service.ts b/projects/backend/src/service/score.service.ts index 82de6d8..784f446 100644 --- a/projects/backend/src/service/score.service.ts +++ b/projects/backend/src/service/score.service.ts @@ -7,7 +7,6 @@ import BeatSaverService from "./beatsaver.service"; import ScoreSaberLeaderboard, { getScoreSaberLeaderboardFromToken, } from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; -import { getScoreSaberScoreFromToken } from "@ssr/common/score/impl/scoresaber-score"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { ScoreSort } from "@ssr/common/score/score-sort"; import { Leaderboards } from "@ssr/common/leaderboard"; @@ -16,7 +15,6 @@ import LeaderboardService from "./leaderboard.service"; import { BeatSaverMap } from "@ssr/common/model/beatsaver/map"; import { PlayerScore } from "@ssr/common/score/player-score"; import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response"; -import Score from "@ssr/common/score/score"; import PlayerScoresResponse from "@ssr/common/response/player-scores-response"; import { DiscordChannels, logToChannel } from "../bot/bot"; import { EmbedBuilder } from "discord.js"; @@ -30,6 +28,8 @@ import { AdditionalScoreDataModel, } from "@ssr/common/model/additional-score-data/additional-score-data"; import { BeatLeaderScoreImprovementToken } from "@ssr/common/types/token/beatleader/score/score-improvement"; +import { getScoreSaberScoreFromToken, ScoreSaberScoreModel } from "@ssr/common/model/score/impl/scoresaber-score"; +import { ScoreType } from "@ssr/common/model/score/score"; const playerScoresCache = new SSRCache({ ttl: 1000 * 60, // 1 minute @@ -52,7 +52,7 @@ export class ScoreService { } const { score: scoreToken, leaderboard: leaderboardToken } = playerScore; - const score = getScoreSaberScoreFromToken(scoreToken, leaderboardToken); + const score = getScoreSaberScoreFromToken(scoreToken, leaderboardToken, scoreToken.leaderboardPlayerInfo.id); const leaderboard = getScoreSaberLeaderboardFromToken(leaderboardToken); const playerInfo = score.playerInfo; @@ -153,8 +153,13 @@ export class ScoreService { player.markModified("statisticHistory"); await player.save(); + const scoreToken = getScoreSaberScoreFromToken(score, leaderboard, playerId); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-expect-error + delete scoreToken.playerInfo; + await ScoreSaberScoreModel.create(scoreToken); console.log( - `Updated scores set statistic for "${playerName}"(${playerId}), scores today: ${scores.rankedScores} ranked, ${scores.unrankedScores} unranked` + `Tracked score and updated scores set statistic for "${playerName}"(${playerId}), scores today: ${scores.rankedScores} ranked, ${scores.unrankedScores} unranked` ); } @@ -268,7 +273,7 @@ export class ScoreService { * Gets scores for a player. * * @param leaderboardName the leaderboard to get the scores from - * @param id the players id + * @param playerId the players id * @param page the page to get * @param sort the sort to use * @param search the search to use @@ -276,14 +281,14 @@ export class ScoreService { */ public static async getPlayerScores( leaderboardName: Leaderboards, - id: string, + playerId: string, page: number, sort: string, search?: string ): Promise | undefined> { return fetchWithCache( playerScoresCache, - `player-scores-${leaderboardName}-${id}-${page}-${sort}-${search}`, + `player-scores-${leaderboardName}-${playerId}-${page}-${sort}-${search}`, async () => { const scores: PlayerScore[] | undefined = []; let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values @@ -291,7 +296,7 @@ export class ScoreService { switch (leaderboardName) { case "scoresaber": { const leaderboardScores = await scoresaberService.lookupPlayerScores({ - playerId: id, + playerId: playerId, page: page, sort: sort as ScoreSort, search: search, @@ -308,17 +313,18 @@ export class ScoreService { ); for (const token of leaderboardScores.playerScores) { - const score = getScoreSaberScoreFromToken(token.score, token.leaderboard); + const score = getScoreSaberScoreFromToken(token.score, token.leaderboard, playerId); if (score == undefined) { continue; } + const leaderboard = getScoreSaberLeaderboardFromToken(token.leaderboard); if (leaderboard == undefined) { continue; } const additionalData = await this.getAdditionalScoreData( - id, + playerId, leaderboard.songHash, `${leaderboard.difficulty.difficulty}-${leaderboard.difficulty.characteristic}`, score.score @@ -352,65 +358,73 @@ export class ScoreService { * Gets scores for a leaderboard. * * @param leaderboardName the leaderboard to get the scores from - * @param id the leaderboard id + * @param leaderboardId the leaderboard id * @param page the page to get * @returns the scores */ public static async getLeaderboardScores( leaderboardName: Leaderboards, - id: string, + leaderboardId: string, page: number ): Promise | undefined> { - return fetchWithCache(leaderboardScoresCache, `leaderboard-scores-${leaderboardName}-${id}-${page}`, async () => { - const scores: Score[] = []; - let leaderboard: Leaderboard | undefined; - let beatSaverMap: BeatSaverMap | undefined; - let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values + return fetchWithCache( + leaderboardScoresCache, + `leaderboard-scores-${leaderboardName}-${leaderboardId}-${page}`, + async () => { + const scores: ScoreType[] = []; + let leaderboard: Leaderboard | undefined; + let beatSaverMap: BeatSaverMap | undefined; + let metadata: Metadata = new Metadata(0, 0, 0, 0); // Default values - switch (leaderboardName) { - case "scoresaber": { - const leaderboardResponse = await LeaderboardService.getLeaderboard( - leaderboardName, - id - ); - if (leaderboardResponse == undefined) { - throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`); - } - leaderboard = leaderboardResponse.leaderboard; - beatSaverMap = leaderboardResponse.beatsaver; + switch (leaderboardName) { + case "scoresaber": { + const leaderboardResponse = await LeaderboardService.getLeaderboard( + leaderboardName, + leaderboardId + ); + if (leaderboardResponse == undefined) { + throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`); + } + leaderboard = leaderboardResponse.leaderboard; + beatSaverMap = leaderboardResponse.beatsaver; - const leaderboardScores = await scoresaberService.lookupLeaderboardScores(id, page); - if (leaderboardScores == undefined) { + const leaderboardScores = await scoresaberService.lookupLeaderboardScores(leaderboardId, page); + if (leaderboardScores == undefined) { + break; + } + + for (const token of leaderboardScores.scores) { + const score = getScoreSaberScoreFromToken( + token, + leaderboardResponse.leaderboard, + token.leaderboardPlayerInfo.id + ); + if (score == undefined) { + continue; + } + scores.push(score); + } + + metadata = new Metadata( + Math.ceil(leaderboardScores.metadata.total / leaderboardScores.metadata.itemsPerPage), + leaderboardScores.metadata.total, + leaderboardScores.metadata.page, + leaderboardScores.metadata.itemsPerPage + ); break; } - - for (const token of leaderboardScores.scores) { - const score = getScoreSaberScoreFromToken(token, leaderboardResponse.leaderboard); - if (score == undefined) { - continue; - } - scores.push(score); + default: { + throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`); } + } - metadata = new Metadata( - Math.ceil(leaderboardScores.metadata.total / leaderboardScores.metadata.itemsPerPage), - leaderboardScores.metadata.total, - leaderboardScores.metadata.page, - leaderboardScores.metadata.itemsPerPage - ); - break; - } - default: { - throw new NotFoundError(`Leaderboard "${leaderboardName}" not found`); - } + return { + scores: scores, + leaderboard: leaderboard, + beatSaver: beatSaverMap, + metadata: metadata, + }; } - - return { - scores: scores, - leaderboard: leaderboard, - beatSaver: beatSaverMap, - metadata: metadata, - }; - }); + ); } } diff --git a/projects/common/package.json b/projects/common/package.json index 7a123a8..900637b 100644 --- a/projects/common/package.json +++ b/projects/common/package.json @@ -19,6 +19,7 @@ "typescript": "^5" }, "dependencies": { + "@typegoose/auto-increment": "^4.7.0", "@typegoose/typegoose": "^12.8.0", "ky": "^1.7.2", "ws": "^8.18.0" diff --git a/projects/common/src/model/additional-score-data/additional-score-data.ts b/projects/common/src/model/additional-score-data/additional-score-data.ts index 98f79a6..579eaf0 100644 --- a/projects/common/src/model/additional-score-data/additional-score-data.ts +++ b/projects/common/src/model/additional-score-data/additional-score-data.ts @@ -4,7 +4,7 @@ import { HandAccuracy } from "./hand-accuracy"; import { Misses } from "./misses"; /** - * The model for a BeatSaver map. + * The model for additional score data. */ @modelOptions({ options: { allowMixed: Severity.ALLOW }, diff --git a/projects/common/src/model/score/impl/scoresaber-score.ts b/projects/common/src/model/score/impl/scoresaber-score.ts new file mode 100644 index 0000000..d36ea1e --- /dev/null +++ b/projects/common/src/model/score/impl/scoresaber-score.ts @@ -0,0 +1,123 @@ +import { getModelForClass, modelOptions, plugin, Prop, ReturnModelType, Severity } from "@typegoose/typegoose"; +import Score from "../score"; +import { Modifier } from "../../../score/modifier"; +import ScoreSaberScoreToken from "../../../types/token/scoresaber/score-saber-score-token"; +import ScoreSaberLeaderboardToken from "../../../types/token/scoresaber/score-saber-leaderboard-token"; +import ScoreSaberLeaderboard from "../../../leaderboard/impl/scoresaber-leaderboard"; +import { type ScoreSaberLeaderboardPlayerInfoToken } from "../../../types/token/scoresaber/score-saber-leaderboard-player-info-token"; +import { Document } from "mongoose"; +import { AutoIncrementID } from "@typegoose/auto-increment"; + +@modelOptions({ + options: { allowMixed: Severity.ALLOW }, + schemaOptions: { + collection: "scoresaber-scores", + toObject: { + virtuals: true, + transform: function (_, ret) { + ret.id = ret._id; + delete ret._id; + delete ret.__v; + return ret; + }, + }, + }, +}) +@plugin(AutoIncrementID, { + field: "_id", + startAt: 1, + trackerModelName: "scores", + trackerCollection: "increments", + overwriteModelName: "scoresaber-scores", +}) +export class ScoreSaberScoreInternal extends Score { + /** + * The score's id. + */ + @Prop({ required: true, index: true }) + public readonly scoreId!: string; + + /** + * The leaderboard the score was set on. + */ + @Prop({ required: true, index: true }) + public readonly leaderboardId!: number; + + /** + * The amount of pp for the score. + * @private + */ + @Prop({ required: true }) + public readonly pp!: number; + + /** + * The weight of the score, or undefined if not ranked. + * @private + */ + @Prop() + public readonly weight?: number; + + /** + * The max combo of the score. + */ + @Prop({ required: true }) + public readonly maxCombo!: number; +} + +class ScoreSaberScorePublic extends ScoreSaberScoreInternal { + /** + * The player who set the score. + */ + public playerInfo!: ScoreSaberLeaderboardPlayerInfoToken; +} + +export type ScoreSaberScore = InstanceType; + +/** + * Gets a {@link ScoreSaberScore} from a {@link ScoreSaberScoreToken}. + * + * @param token the token to convert + * @param playerId the id of the player who set the score + * @param leaderboard the leaderboard the score was set on + */ +export function getScoreSaberScoreFromToken( + token: ScoreSaberScoreToken, + leaderboard: ScoreSaberLeaderboardToken | ScoreSaberLeaderboard, + playerId?: string +): ScoreSaberScore { + const modifiers: Modifier[] = + token.modifiers == undefined || token.modifiers === "" + ? [] + : token.modifiers.split(",").map(mod => { + mod = mod.toUpperCase(); + const modifier = Modifier[mod as keyof typeof Modifier]; + if (modifier === undefined) { + throw new Error(`Unknown modifier: ${mod}`); + } + return modifier; + }); + + return { + leaderboard: "scoresaber", + playerId: playerId || token.leaderboardPlayerInfo.id, + leaderboardId: leaderboard.id, + score: token.baseScore, + accuracy: (token.baseScore / leaderboard.maxScore) * 100, + rank: token.rank, + modifiers: modifiers, + misses: token.missedNotes + token.badCuts, + missedNotes: token.missedNotes, + badCuts: token.badCuts, + fullCombo: token.fullCombo, + timestamp: new Date(token.timeSet), + scoreId: token.id, + pp: token.pp, + weight: token.weight, + maxCombo: token.maxCombo, + playerInfo: token.leaderboardPlayerInfo, + }; +} + +export type ScoreSaberScoreDocument = ScoreSaberScore & Document; +export const ScoreSaberScoreModel: ReturnModelType = + getModelForClass(ScoreSaberScoreInternal); diff --git a/projects/common/src/model/score/score.ts b/projects/common/src/model/score/score.ts new file mode 100644 index 0000000..39aa040 --- /dev/null +++ b/projects/common/src/model/score/score.ts @@ -0,0 +1,96 @@ +import { Modifier } from "../../score/modifier"; +import { AdditionalScoreData } from "../additional-score-data/additional-score-data"; +import { type Leaderboards } from "../../leaderboard"; +import { prop } from "@typegoose/typegoose"; + +/** + * The model for a score. + */ +export default class Score { + /** + * The internal score id. + */ + @prop() + private _id?: number; + + /** + * The leaderboard the score is from. + */ + @prop({ required: true }) + public readonly leaderboard!: Leaderboards; + + /** + * The id of the player who set the score. + * @private + */ + @prop({ required: true, index: true }) + public readonly playerId!: string; + + /** + * The base score for the score. + * @private + */ + @prop({ required: true }) + public readonly score!: number; + + /** + * The accuracy of the score. + */ + @prop({ required: true }) + public readonly accuracy!: number; + + /** + * The rank for the score. + * @private + */ + @prop({ required: true }) + public readonly rank!: number; + + /** + * The modifiers used on the score. + * @private + */ + @prop({ enum: () => Modifier, type: String, required: true }) + public readonly modifiers!: Modifier[]; + + /** + * The total amount of misses. + * @private + */ + @prop({ required: true }) + public readonly misses!: number; + + /** + * The amount of missed notes. + */ + @prop({ required: true }) + public readonly missedNotes!: number; + + /** + * The amount of bad cuts. + * @private + */ + @prop({ required: true }) + public readonly badCuts!: number; + + /** + * Whether every note was hit. + * @private + */ + @prop({ required: true }) + public readonly fullCombo!: boolean; + + /** + * The additional data for the score. + */ + public additionalData?: AdditionalScoreData; + + /** + * The time the score was set. + * @private + */ + @prop({ required: true }) + public readonly timestamp!: Date; +} + +export type ScoreType = InstanceType; diff --git a/projects/common/src/score/impl/scoresaber-score.ts b/projects/common/src/score/impl/scoresaber-score.ts deleted file mode 100644 index 4706a17..0000000 --- a/projects/common/src/score/impl/scoresaber-score.ts +++ /dev/null @@ -1,76 +0,0 @@ -import Score from "../score"; -import { Modifier } from "../modifier"; -import ScoreSaberScoreToken from "../../types/token/scoresaber/score-saber-score-token"; -import ScoreSaberLeaderboardPlayerInfoToken from "../../types/token/scoresaber/score-saber-leaderboard-player-info-token"; -import ScoreSaberLeaderboardToken from "../../types/token/scoresaber/score-saber-leaderboard-token"; -import ScoreSaberLeaderboard from "../../leaderboard/impl/scoresaber-leaderboard"; - -export default interface ScoreSaberScore extends Score { - /** - * The score's id. - */ - readonly id: string; - - /** - * The amount of pp for the score. - * @private - */ - readonly pp: number; - - /** - * The weight of the score, or undefined if not ranked.s - * @private - */ - readonly weight?: number; - - /** - * The max combo of the score. - */ - readonly maxCombo: number; - - /** - * The player who set the score - */ - readonly playerInfo: ScoreSaberLeaderboardPlayerInfoToken; -} - -/** - * Gets a {@link ScoreSaberScore} from a {@link ScoreSaberScoreToken}. - * - * @param token the token to convert - * @param leaderboard the leaderboard the score was set on - */ -export function getScoreSaberScoreFromToken( - token: ScoreSaberScoreToken, - leaderboard?: ScoreSaberLeaderboardToken | ScoreSaberLeaderboard -): ScoreSaberScore { - const modifiers: Modifier[] = - token.modifiers == undefined || token.modifiers === "" - ? [] - : token.modifiers.split(",").map(mod => { - mod = mod.toUpperCase(); - const modifier = Modifier[mod as keyof typeof Modifier]; - if (modifier === undefined) { - throw new Error(`Unknown modifier: ${mod}`); - } - return modifier; - }); - - return { - leaderboard: "scoresaber", - score: token.baseScore, - accuracy: leaderboard ? (token.baseScore / leaderboard.maxScore) * 100 : Infinity, - rank: token.rank, - modifiers: modifiers, - misses: token.missedNotes + token.badCuts, - missedNotes: token.missedNotes, - badCuts: token.badCuts, - fullCombo: token.fullCombo, - timestamp: new Date(token.timeSet), - id: token.id, - pp: token.pp, - weight: token.weight, - maxCombo: token.maxCombo, - playerInfo: token.leaderboardPlayerInfo, - }; -} diff --git a/projects/common/src/score/score.ts b/projects/common/src/score/score.ts deleted file mode 100644 index fc78648..0000000 --- a/projects/common/src/score/score.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { Modifier } from "./modifier"; -import { Leaderboards } from "../leaderboard"; -import { AdditionalScoreData } from "../model/additional-score-data/additional-score-data"; - -export default interface Score { - /** - * The leaderboard the score is from. - */ - readonly leaderboard: Leaderboards; - - /** - * The base score for the score. - * @private - */ - readonly score: number; - - /** - * The accuracy of the score. - */ - readonly accuracy: number; - - /** - * The rank for the score. - * @private - */ - readonly rank: number; - - /** - * The modifiers used on the score. - * @private - */ - readonly modifiers: Modifier[]; - - /** - * The amount total amount of misses. - * @private - */ - readonly misses: number; - - /** - * The amount of missed notes. - */ - readonly missedNotes: number; - - /** - * The amount of bad cuts. - * @private - */ - readonly badCuts: number; - - /** - * Whether every note was hit. - * @private - */ - readonly fullCombo: boolean; - - /** - * The additional data for the score. - */ - additionalData?: AdditionalScoreData; - - /** - * The time the score was set. - * @private - */ - readonly timestamp: Date; -} diff --git a/projects/common/src/types/token/scoresaber/score-saber-leaderboard-player-info-token.ts b/projects/common/src/types/token/scoresaber/score-saber-leaderboard-player-info-token.ts index f450c58..e6569b0 100644 --- a/projects/common/src/types/token/scoresaber/score-saber-leaderboard-player-info-token.ts +++ b/projects/common/src/types/token/scoresaber/score-saber-leaderboard-player-info-token.ts @@ -1,8 +1,8 @@ -export default interface ScoreSaberLeaderboardPlayerInfoToken { +export type ScoreSaberLeaderboardPlayerInfoToken = { id: string; name: string; profilePicture: string; country: string; permissions: number; role: string; -} +}; diff --git a/projects/common/src/types/token/scoresaber/score-saber-score-token.ts b/projects/common/src/types/token/scoresaber/score-saber-score-token.ts index 215a2c1..322411e 100644 --- a/projects/common/src/types/token/scoresaber/score-saber-score-token.ts +++ b/projects/common/src/types/token/scoresaber/score-saber-score-token.ts @@ -1,5 +1,5 @@ import ScoreSaberLeaderboardToken from "./score-saber-leaderboard-token"; -import ScoreSaberLeaderboardPlayerInfoToken from "./score-saber-leaderboard-player-info-token"; +import { ScoreSaberLeaderboardPlayerInfoToken } from "./score-saber-leaderboard-player-info-token"; export default interface ScoreSaberScoreToken { id: string; diff --git a/projects/common/src/utils/scoresaber.util.ts b/projects/common/src/utils/scoresaber.util.ts index 8b135ab..829480f 100644 --- a/projects/common/src/utils/scoresaber.util.ts +++ b/projects/common/src/utils/scoresaber.util.ts @@ -1,6 +1,6 @@ import ScoreSaberPlayerToken from "../types/token/scoresaber/score-saber-player-token"; -import ScoreSaberLeaderboardPlayerInfoToken from "../types/token/scoresaber/score-saber-leaderboard-player-info-token"; import ScoreSaberPlayer from "../player/impl/scoresaber-player"; +import { ScoreSaberLeaderboardPlayerInfoToken } from "../types/token/scoresaber/score-saber-leaderboard-player-info-token"; export type ScoreSaberRole = { /** diff --git a/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx b/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx index a6e7d48..0efa0db 100644 --- a/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx +++ b/projects/website/src/app/(pages)/leaderboard/[...slug]/page.tsx @@ -4,7 +4,7 @@ import { Colors } from "@/common/colors"; import { getAverageColor } from "@/common/image-utils"; import { LeaderboardData } from "@/components/leaderboard/leaderboard-data"; import { Config } from "@ssr/common/config"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import { LeaderboardResponse } from "@ssr/common/response/leaderboard-response"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import { fetchLeaderboard } from "@ssr/common/utils/leaderboard.util"; diff --git a/projects/website/src/app/(pages)/player/[...slug]/page.tsx b/projects/website/src/app/(pages)/player/[...slug]/page.tsx index f5cc071..058a7eb 100644 --- a/projects/website/src/app/(pages)/player/[...slug]/page.tsx +++ b/projects/website/src/app/(pages)/player/[...slug]/page.tsx @@ -8,7 +8,7 @@ import { getCookieValue } from "@ssr/common/utils/cookie-utils"; import { Config } from "@ssr/common/config"; import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/player/impl/scoresaber-player"; import { ScoreSort } from "@ssr/common/score/score-sort"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import { fetchPlayerScores } from "@ssr/common/utils/score-utils"; import PlayerScoresResponse from "@ssr/common/response/player-scores-response"; diff --git a/projects/website/src/components/leaderboard/chart/player-score-accuracy-chart.tsx b/projects/website/src/components/leaderboard/chart/player-score-accuracy-chart.tsx index 96c1def..4879a29 100644 --- a/projects/website/src/components/leaderboard/chart/player-score-accuracy-chart.tsx +++ b/projects/website/src/components/leaderboard/chart/player-score-accuracy-chart.tsx @@ -1,6 +1,5 @@ "use client"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; import { ScoreStatsToken } from "@ssr/common/types/token/beatleader/score-stats/score-stats"; import { formatTime } from "@ssr/common/utils/time-utils"; import GenericChart, { DatasetConfig } from "@/components/chart/generic-chart"; diff --git a/projects/website/src/components/leaderboard/leaderboard-data.tsx b/projects/website/src/components/leaderboard/leaderboard-data.tsx index 16ea3a7..9538aa9 100644 --- a/projects/website/src/components/leaderboard/leaderboard-data.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-data.tsx @@ -2,7 +2,7 @@ import LeaderboardScores from "@/components/leaderboard/leaderboard-scores"; import { LeaderboardInfo } from "@/components/leaderboard/leaderboard-info"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import { LeaderboardResponse } from "@ssr/common/response/leaderboard-response"; import { useQuery } from "@tanstack/react-query"; diff --git a/projects/website/src/components/leaderboard/leaderboard-score.tsx b/projects/website/src/components/leaderboard/leaderboard-score.tsx index d7b45a0..ea7b320 100644 --- a/projects/website/src/components/leaderboard/leaderboard-score.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-score.tsx @@ -1,4 +1,4 @@ -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import { formatNumberWithCommas, formatPp } from "@ssr/common/utils/number-utils"; import ScoreSaberPlayerToken from "@ssr/common/types/token/scoresaber/score-saber-player-token"; import { PlayerInfo } from "@/components/player/player-info"; diff --git a/projects/website/src/components/leaderboard/leaderboard-scores.tsx b/projects/website/src/components/leaderboard/leaderboard-scores.tsx index ddab1ed..cba964b 100644 --- a/projects/website/src/components/leaderboard/leaderboard-scores.tsx +++ b/projects/website/src/components/leaderboard/leaderboard-scores.tsx @@ -10,7 +10,7 @@ import { scoreAnimation } from "@/components/score/score-animation"; import { Button } from "@/components/ui/button"; import { getDifficulty } from "@/common/song-utils"; import { fetchLeaderboardScores } from "@ssr/common/utils/score-utils"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response"; import useDatabase from "@/hooks/use-database"; diff --git a/projects/website/src/components/player/player-data.tsx b/projects/website/src/components/player/player-data.tsx index b83bc8f..f1cfccd 100644 --- a/projects/website/src/components/player/player-data.tsx +++ b/projects/website/src/components/player/player-data.tsx @@ -15,7 +15,7 @@ import useDatabase from "@/hooks/use-database"; import { useLiveQuery } from "dexie-react-hooks"; import ScoreSaberPlayer, { getScoreSaberPlayerFromToken } from "@ssr/common/player/impl/scoresaber-player"; import { ScoreSort } from "@ssr/common/score/score-sort"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import PlayerScoresResponse from "@ssr/common/response/player-scores-response"; diff --git a/projects/website/src/components/player/player-scores.tsx b/projects/website/src/components/player/player-scores.tsx index d73d2e5..26ade96 100644 --- a/projects/website/src/components/player/player-scores.tsx +++ b/projects/website/src/components/player/player-scores.tsx @@ -15,7 +15,7 @@ import { scoreAnimation } from "@/components/score/score-animation"; import ScoreSaberPlayer from "@ssr/common/player/impl/scoresaber-player"; import { ScoreSort } from "@ssr/common/score/score-sort"; import { setCookieValue } from "@ssr/common/utils/cookie-utils"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import { fetchPlayerScores } from "@ssr/common/utils/score-utils"; import PlayerScoresResponse from "@ssr/common/response/player-scores-response"; diff --git a/projects/website/src/components/score/badges/badge-props.ts b/projects/website/src/components/score/badges/badge-props.ts index 1860ce0..72c4fd1 100644 --- a/projects/website/src/components/score/badges/badge-props.ts +++ b/projects/website/src/components/score/badges/badge-props.ts @@ -1,4 +1,4 @@ -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; export type ScoreBadgeProps = { /** diff --git a/projects/website/src/components/score/score-badge.tsx b/projects/website/src/components/score/score-badge.tsx index ea1b710..2f7851c 100644 --- a/projects/website/src/components/score/score-badge.tsx +++ b/projects/website/src/components/score/score-badge.tsx @@ -1,5 +1,5 @@ import StatValue from "@/components/stat-value"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; /** diff --git a/projects/website/src/components/score/score-buttons.tsx b/projects/website/src/components/score/score-buttons.tsx index fbc25cd..aa391c0 100644 --- a/projects/website/src/components/score/score-buttons.tsx +++ b/projects/website/src/components/score/score-buttons.tsx @@ -10,7 +10,7 @@ import { copyToClipboard } from "@/common/browser-utils"; import { ArrowDownIcon } from "@heroicons/react/24/solid"; import clsx from "clsx"; import ScoreEditorButton from "@/components/score/score-editor-button"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import { BeatSaverMap } from "@ssr/common/model/beatsaver/map"; import BeatSaberPepeLogo from "@/components/logos/beatsaber-pepe-logo"; diff --git a/projects/website/src/components/score/score-editor-button.tsx b/projects/website/src/components/score/score-editor-button.tsx index 6b555a8..d784d25 100644 --- a/projects/website/src/components/score/score-editor-button.tsx +++ b/projects/website/src/components/score/score-editor-button.tsx @@ -6,7 +6,7 @@ import { Slider } from "@/components/ui/slider"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { ResetIcon } from "@radix-ui/react-icons"; import Tooltip from "@/components/tooltip"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; type ScoreEditorButtonProps = { diff --git a/projects/website/src/components/score/score-feed/score-feed.tsx b/projects/website/src/components/score/score-feed/score-feed.tsx index d735168..8588c08 100644 --- a/projects/website/src/components/score/score-feed/score-feed.tsx +++ b/projects/website/src/components/score/score-feed/score-feed.tsx @@ -7,7 +7,7 @@ import { parseDate } from "@ssr/common/utils/time-utils"; import Link from "next/link"; import useWebSocket, { ReadyState } from "react-use-websocket"; import { ScoreSaberWebsocketMessageToken } from "@ssr/common/types/token/scoresaber/websocket/scoresaber-websocket-message"; -import { getScoreSaberScoreFromToken } from "@ssr/common/score/impl/scoresaber-score"; +import { getScoreSaberScoreFromToken } from "@ssr/common/model/score/impl/scoresaber-score"; import { getScoreSaberLeaderboardFromToken } from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; export default function ScoreFeed() { diff --git a/projects/website/src/components/score/score-modifiers.tsx b/projects/website/src/components/score/score-modifiers.tsx index 9a075a6..d59fdaa 100644 --- a/projects/website/src/components/score/score-modifiers.tsx +++ b/projects/website/src/components/score/score-modifiers.tsx @@ -1,4 +1,4 @@ -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import { Modifier } from "@ssr/common/score/modifier"; type ScoreModifiersProps = { diff --git a/projects/website/src/components/score/score-rank-info.tsx b/projects/website/src/components/score/score-rank-info.tsx index 5d278c2..97bd291 100644 --- a/projects/website/src/components/score/score-rank-info.tsx +++ b/projects/website/src/components/score/score-rank-info.tsx @@ -2,7 +2,7 @@ import { formatNumberWithCommas } from "@ssr/common/utils/number-utils"; import { GlobeAmericasIcon } from "@heroicons/react/24/solid"; import Link from "next/link"; import { getPageFromRank } from "@ssr/common/utils/utils"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import { ScoreTimeSet } from "@/components/score/score-time-set"; diff --git a/projects/website/src/components/score/score-stats.tsx b/projects/website/src/components/score/score-stats.tsx index e0daede..8020412 100644 --- a/projects/website/src/components/score/score-stats.tsx +++ b/projects/website/src/components/score/score-stats.tsx @@ -1,6 +1,6 @@ import { getScoreBadgeFromAccuracy } from "@/common/song-utils"; import { ScoreBadge, ScoreBadges } from "@/components/score/score-badge"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import ScoreMissesBadge from "@/components/score/badges/score-misses"; import { HandAccuracyBadge } from "@/components/score/badges/hand-accuracy"; diff --git a/projects/website/src/components/score/score-time-set.tsx b/projects/website/src/components/score/score-time-set.tsx index 29995ca..0127712 100644 --- a/projects/website/src/components/score/score-time-set.tsx +++ b/projects/website/src/components/score/score-time-set.tsx @@ -1,7 +1,7 @@ import { format } from "@formkit/tempo"; import { timeAgo } from "@ssr/common/utils/time-utils"; import Tooltip from "@/components/tooltip"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; type ScoreTimeSetProps = { /** diff --git a/projects/website/src/components/score/score.tsx b/projects/website/src/components/score/score.tsx index 51d4bd7..d3b6f7f 100644 --- a/projects/website/src/components/score/score.tsx +++ b/projects/website/src/components/score/score.tsx @@ -9,7 +9,7 @@ import ScoreStats from "./score-stats"; import { motion } from "framer-motion"; import { getPageFromRank } from "@ssr/common/utils/utils"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; -import ScoreSaberScore from "@ssr/common/score/impl/scoresaber-score"; +import { ScoreSaberScore } from "@ssr/common/model/score/impl/scoresaber-score"; import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; import { BeatSaverMap } from "@ssr/common/model/beatsaver/map"; import { useIsMobile } from "@/hooks/use-is-mobile";