migrate some values to ssr data tracking so we don't need to rely on BL as much
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Successful in 49s
Deploy Website / docker (ubuntu-latest) (push) Has been cancelled

This commit is contained in:
Lee 2024-10-26 18:41:51 +01:00
parent 5ff0d11f5a
commit 9626931b91
8 changed files with 196 additions and 68 deletions

@ -71,6 +71,6 @@ export default class ScoresController {
}; };
query: { search?: string }; query: { search?: string };
}): Promise<unknown> { }): Promise<unknown> {
return (await ScoreService.getPreviousScores(playerId, leaderboardId, page)).toJSON(); return (await ScoreService.getScoreHistory(playerId, leaderboardId, page)).toJSON();
} }
} }

@ -61,17 +61,19 @@ connectBeatLeaderWebsocket({
}); });
export const app = new Elysia(); export const app = new Elysia();
app.use( if (isProduction()) {
app.use(
cron({ cron({
name: "player-statistics-tracker-cron", name: "player-statistics-tracker-cron",
pattern: "1 0 * * *", // Every day at 00:01 pattern: "1 0 * * *", // Every day at 00:01
timezone: "Europe/London", // UTC time timezone: "Europe/London", // UTC time
protect: true,
run: async () => { run: async () => {
await PlayerService.updatePlayerStatistics(); await PlayerService.updatePlayerStatistics();
}, },
}) })
); );
app.use( app.use(
cron({ cron({
name: "player-scores-tracker-cron", name: "player-scores-tracker-cron",
pattern: "0 4 * * *", // Every day at 04:00 pattern: "0 4 * * *", // Every day at 04:00
@ -81,7 +83,8 @@ app.use(
await PlayerService.refreshPlayerScores(); await PlayerService.refreshPlayerScores();
}, },
}) })
); );
}
/** /**
* Custom error handler * Custom error handler

@ -7,7 +7,6 @@ import BeatSaverService from "./beatsaver.service";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import { ScoreSort } from "@ssr/common/score/score-sort"; import { ScoreSort } from "@ssr/common/score/score-sort";
import { Leaderboards } from "@ssr/common/leaderboard"; import { Leaderboards } from "@ssr/common/leaderboard";
import Leaderboard from "@ssr/common/leaderboard/leaderboard";
import LeaderboardService from "./leaderboard.service"; import LeaderboardService from "./leaderboard.service";
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map"; import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
import { PlayerScore } from "@ssr/common/score/player-score"; import { PlayerScore } from "@ssr/common/score/player-score";
@ -27,13 +26,18 @@ import {
import { BeatLeaderScoreImprovementToken } from "@ssr/common/types/token/beatleader/score/score-improvement"; import { BeatLeaderScoreImprovementToken } from "@ssr/common/types/token/beatleader/score/score-improvement";
import { ScoreType } from "@ssr/common/model/score/score"; import { ScoreType } from "@ssr/common/model/score/score";
import { getScoreSaberLeaderboardFromToken, getScoreSaberScoreFromToken } from "@ssr/common/token-creators"; import { getScoreSaberLeaderboardFromToken, getScoreSaberScoreFromToken } from "@ssr/common/token-creators";
import { ScoreSaberScore, ScoreSaberScoreModel } from "@ssr/common/model/score/impl/scoresaber-score"; import {
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard"; ScoreSaberPreviousScore,
ScoreSaberScore,
ScoreSaberScoreModel,
} from "@ssr/common/model/score/impl/scoresaber-score";
import ScoreSaberScoreToken from "@ssr/common/types/token/scoresaber/score-saber-score-token"; import ScoreSaberScoreToken from "@ssr/common/types/token/scoresaber/score-saber-score-token";
import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token"; import ScoreSaberLeaderboardToken from "@ssr/common/types/token/scoresaber/score-saber-leaderboard-token";
import { MapDifficulty } from "@ssr/common/score/map-difficulty"; import { MapDifficulty } from "@ssr/common/score/map-difficulty";
import { MapCharacteristic } from "@ssr/common/types/map-characteristic"; import { MapCharacteristic } from "@ssr/common/types/map-characteristic";
import { Page, Pagination } from "@ssr/common/pagination"; import { Page, Pagination } from "@ssr/common/pagination";
import ScoreSaberLeaderboard from "@ssr/common/model/leaderboard/impl/scoresaber-leaderboard";
import Leaderboard from "@ssr/common/model/leaderboard/leaderboard";
const playerScoresCache = new SSRCache({ const playerScoresCache = new SSRCache({
ttl: 1000 * 60, // 1 minute ttl: 1000 * 60, // 1 minute
@ -394,6 +398,10 @@ export class ScoreService {
if (additionalData !== undefined) { if (additionalData !== undefined) {
score.additionalData = additionalData; score.additionalData = additionalData;
} }
const previousScore = await this.getPreviousScore(playerId, leaderboard.id + "", score.timestamp);
if (previousScore !== undefined) {
score.previousScore = previousScore;
}
scores.push({ scores.push({
score: score, score: score,
@ -491,13 +499,13 @@ export class ScoreService {
} }
/** /**
* Gets the previous scores for a player. * Gets the player's score history for a map.
* *
* @param playerId the player's id to get the previous scores for * @param playerId the player's id to get the previous scores for
* @param leaderboardId the leaderboard to get the previous scores on * @param leaderboardId the leaderboard to get the previous scores on
* @param page the page to get * @param page the page to get
*/ */
public static async getPreviousScores( public static async getScoreHistory(
playerId: string, playerId: string,
leaderboardId: string, leaderboardId: string,
page: number page: number
@ -533,6 +541,10 @@ export class ScoreService {
if (additionalData !== undefined) { if (additionalData !== undefined) {
score.additionalData = additionalData; score.additionalData = additionalData;
} }
const previousScore = await this.getPreviousScore(playerId, leaderboardId, score.timestamp);
if (previousScore !== undefined) {
score.previousScore = previousScore;
}
toReturn.push({ toReturn.push({
score: score as unknown as ScoreSaberScore, score: score as unknown as ScoreSaberScore,
@ -544,4 +556,55 @@ export class ScoreService {
return toReturn; return toReturn;
}); });
} }
/**
* Gets the player's previous score for a map.
*
* @param playerId the player's id to get the previous score for
* @param leaderboardId the leaderboard to get the previous score on
* @param timestamp the score's timestamp to get the previous score for
* @returns the score, or undefined if none
*/
public static async getPreviousScore(
playerId: string,
leaderboardId: string,
timestamp: Date
): Promise<ScoreSaberPreviousScore | undefined> {
const scores = await ScoreSaberScoreModel.find({ playerId: playerId, leaderboardId: leaderboardId });
if (scores == null || scores.length == 0) {
return undefined;
}
const scoreIndex = scores.findIndex(score => score.timestamp.getTime() == timestamp.getTime());
const score = scores.find(score => score.timestamp.getTime() == timestamp.getTime());
if (scoreIndex == -1 || score == undefined) {
return undefined;
}
const previousScore = scores[scoreIndex - 1];
if (previousScore == undefined) {
return undefined;
}
return {
score: previousScore.score,
accuracy: previousScore.accuracy,
modifiers: previousScore.modifiers,
misses: previousScore.misses,
missedNotes: previousScore.missedNotes,
badCuts: previousScore.badCuts,
fullCombo: previousScore.fullCombo,
pp: previousScore.pp,
weight: previousScore.weight,
maxCombo: previousScore.maxCombo,
change: {
score: score.score - previousScore.score,
accuracy: score.accuracy - previousScore.accuracy,
misses: score.misses - previousScore.misses,
missedNotes: score.missedNotes - previousScore.missedNotes,
badCuts: score.badCuts - previousScore.badCuts,
pp: score.pp - previousScore.pp,
weight: score.weight && previousScore.weight && score.weight - previousScore.weight,
maxCombo: score.maxCombo - previousScore.maxCombo,
},
} as ScoreSaberPreviousScore;
}
} }

@ -3,6 +3,7 @@ import Score from "../score";
import { type ScoreSaberLeaderboardPlayerInfoToken } from "../../../types/token/scoresaber/score-saber-leaderboard-player-info-token"; import { type ScoreSaberLeaderboardPlayerInfoToken } from "../../../types/token/scoresaber/score-saber-leaderboard-player-info-token";
import { Document } from "mongoose"; import { Document } from "mongoose";
import { AutoIncrementID } from "@typegoose/auto-increment"; import { AutoIncrementID } from "@typegoose/auto-increment";
import { PreviousScore } from "../previous-score";
@modelOptions({ @modelOptions({
options: { allowMixed: Severity.ALLOW }, options: { allowMixed: Severity.ALLOW },
@ -58,6 +59,11 @@ export class ScoreSaberScoreInternal extends Score {
*/ */
@Prop({ required: true }) @Prop({ required: true })
public readonly maxCombo!: number; public readonly maxCombo!: number;
/**
* The previous score, if any.
*/
public previousScore?: ScoreSaberPreviousScore;
} }
class ScoreSaberScorePublic extends ScoreSaberScoreInternal { class ScoreSaberScorePublic extends ScoreSaberScoreInternal {
@ -67,6 +73,28 @@ class ScoreSaberScorePublic extends ScoreSaberScoreInternal {
public playerInfo!: ScoreSaberLeaderboardPlayerInfoToken; public playerInfo!: ScoreSaberLeaderboardPlayerInfoToken;
} }
export type ScoreSaberPreviousScore = PreviousScore & {
/**
* The pp of the previous score.
*/
pp: number;
/**
* The weight of the previous score.
*/
weight: number;
/**
* The max combo of the previous score.
*/
maxCombo: number;
/**
* The change between the previous score and the current score.
*/
change?: ScoreSaberPreviousScore;
};
export type ScoreSaberScore = InstanceType<typeof ScoreSaberScorePublic>; export type ScoreSaberScore = InstanceType<typeof ScoreSaberScorePublic>;
export type ScoreSaberScoreDocument = ScoreSaberScore & Document; export type ScoreSaberScoreDocument = ScoreSaberScore & Document;
export const ScoreSaberScoreModel: ReturnModelType<typeof ScoreSaberScoreInternal> = export const ScoreSaberScoreModel: ReturnModelType<typeof ScoreSaberScoreInternal> =

@ -0,0 +1,38 @@
import { Modifier } from "../../score/modifier";
export type PreviousScore = {
/**
* The score of the previous score.
*/
score: number;
/**
* The accuracy of the previous score.
*/
accuracy: number;
/**
* The modifiers of the previous score.
*/
modifiers?: Modifier[];
/**
* The misses of the previous score.
*/
misses: number;
/**
* The missed notes of the previous score.
*/
missedNotes: number;
/**
* The bad cuts of the previous score.
*/
badCuts: number;
/**
* The full combo of the previous score.
*/
fullCombo?: boolean;
};

@ -14,8 +14,7 @@ type ScoreAccuracyProps = ScoreBadgeProps & {
}; };
export function ScoreAccuracyBadge({ score, leaderboard }: ScoreAccuracyProps) { export function ScoreAccuracyBadge({ score, leaderboard }: ScoreAccuracyProps) {
const scoreImprovement = score.additionalData?.scoreImprovement; const previousScore = score.previousScore;
const previousAccuracy = scoreImprovement ? score.accuracy - scoreImprovement.accuracy : undefined;
const fcAccuracy = score.additionalData?.fcAccuracy; const fcAccuracy = score.additionalData?.fcAccuracy;
const scoreBadge = getScoreBadgeFromAccuracy(score.accuracy); const scoreBadge = getScoreBadgeFromAccuracy(score.accuracy);
@ -57,9 +56,13 @@ export function ScoreAccuracyBadge({ score, leaderboard }: ScoreAccuracyProps) {
{modCount > 0 && <ScoreModifiers type="simple" limit={1} score={score} />} {modCount > 0 && <ScoreModifiers type="simple" limit={1} score={score} />}
</p> </p>
</Tooltip> </Tooltip>
{scoreImprovement && previousAccuracy && ( {previousScore && previousScore.change && (
<Tooltip display={`Previous Accuracy: ${previousAccuracy.toFixed(2)}%`}> <Tooltip display={`Previous Accuracy: ${previousScore.accuracy.toFixed(2)}%`}>
<Change className="text-xs" change={scoreImprovement.accuracy} formatValue={num => `${num.toFixed(2)}%`} /> <Change
className="text-xs"
change={previousScore.change.accuracy}
formatValue={num => `${num.toFixed(2)}%`}
/>
</Tooltip> </Tooltip>
)} )}
</div> </div>

@ -4,7 +4,6 @@ import Tooltip from "@/components/tooltip";
import { ensurePositiveNumber, formatPp } from "@ssr/common/utils/number-utils"; import { ensurePositiveNumber, formatPp } from "@ssr/common/utils/number-utils";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber"; import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import { Change } from "@/common/change"; import { Change } from "@/common/change";
import { Warning } from "@/components/warning";
type ScorePpProps = ScoreBadgeProps & { type ScorePpProps = ScoreBadgeProps & {
/** /**
@ -14,8 +13,7 @@ type ScorePpProps = ScoreBadgeProps & {
}; };
export function ScorePpBadge({ score, leaderboard }: ScorePpProps) { export function ScorePpBadge({ score, leaderboard }: ScorePpProps) {
const scoreImprovement = score.additionalData?.scoreImprovement; const previousScore = score.previousScore;
const previousAccuracy = scoreImprovement ? score.accuracy - scoreImprovement?.accuracy : undefined;
const fcAccuracy = score.additionalData?.fcAccuracy; const fcAccuracy = score.additionalData?.fcAccuracy;
const pp = score.pp; const pp = score.pp;
const weight = score.weight; const weight = score.weight;
@ -28,6 +26,7 @@ export function ScorePpBadge({ score, leaderboard }: ScorePpProps) {
return ( return (
<> <>
<div className="flex flex-col items-center justify-center cursor-default">
<Tooltip <Tooltip
display={ display={
<div className="flex flex-col gap-2"> <div className="flex flex-col gap-2">
@ -39,28 +38,17 @@ export function ScorePpBadge({ score, leaderboard }: ScorePpProps) {
</p> </p>
{fcPp && <p>Full Combo: {fcPp}pp</p>} {fcPp && <p>Full Combo: {fcPp}pp</p>}
</div> </div>
{previousAccuracy && (
<Warning>
<p className="text-red-700">
The previous pp may not be 100% accurate due to ScoreSaber API limitations.
</p>
</Warning>
)}
</div> </div>
} }
> >
<div className="flex flex-col items-center justify-center cursor-default">
<p>{formatPp(pp)}pp</p> <p>{formatPp(pp)}pp</p>
{previousAccuracy && ( </Tooltip>
<Change {previousScore && previousScore.change && (
className="text-xs" <Tooltip display={<p>Previous PP: {formatPp(previousScore.pp)}pp</p>}>
change={ensurePositiveNumber(pp - scoresaberService.getPp(leaderboard.stars, previousAccuracy))} <Change className="text-xs" change={ensurePositiveNumber(previousScore.change.pp)} isPp />
isPp </Tooltip>
/>
)} )}
</div> </div>
</Tooltip>
</> </>
); );
} }

@ -1,14 +1,19 @@
import { ScoreBadgeProps } from "@/components/score/badges/badge-props"; import { ScoreBadgeProps } from "@/components/score/badges/badge-props";
import { formatNumberWithCommas } from "@ssr/common/utils/number-utils"; import { formatNumberWithCommas } from "@ssr/common/utils/number-utils";
import { Change } from "@/common/change"; import { Change } from "@/common/change";
import Tooltip from "@/components/tooltip";
export function ScoreScoreBadge({ score }: ScoreBadgeProps) { export function ScoreScoreBadge({ score }: ScoreBadgeProps) {
const scoreImprovement = score.additionalData?.scoreImprovement; const previousScore = score.previousScore;
return ( return (
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<p>{formatNumberWithCommas(Number(score.score.toFixed(0)))}</p> <p>{formatNumberWithCommas(Number(score.score.toFixed(0)))}</p>
{scoreImprovement && <Change className="text-xs" change={scoreImprovement.score} />} {previousScore && previousScore.change && (
<Tooltip display={<p>Previous Score: {previousScore.score}</p>}>
<Change className="text-xs" change={previousScore.change.score} />
</Tooltip>
)}
</div> </div>
); );
} }