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

View File

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

View File

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

View File

@ -7,7 +7,6 @@ import BeatSaverService from "./beatsaver.service";
import { scoresaberService } from "@ssr/common/service/impl/scoresaber";
import { ScoreSort } from "@ssr/common/score/score-sort";
import { Leaderboards } from "@ssr/common/leaderboard";
import Leaderboard from "@ssr/common/leaderboard/leaderboard";
import LeaderboardService from "./leaderboard.service";
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map";
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 { ScoreType } from "@ssr/common/model/score/score";
import { getScoreSaberLeaderboardFromToken, getScoreSaberScoreFromToken } from "@ssr/common/token-creators";
import { ScoreSaberScore, ScoreSaberScoreModel } from "@ssr/common/model/score/impl/scoresaber-score";
import ScoreSaberLeaderboard from "@ssr/common/leaderboard/impl/scoresaber-leaderboard";
import {
ScoreSaberPreviousScore,
ScoreSaberScore,
ScoreSaberScoreModel,
} from "@ssr/common/model/score/impl/scoresaber-score";
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 { MapDifficulty } from "@ssr/common/score/map-difficulty";
import { MapCharacteristic } from "@ssr/common/types/map-characteristic";
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({
ttl: 1000 * 60, // 1 minute
@ -394,6 +398,10 @@ export class ScoreService {
if (additionalData !== undefined) {
score.additionalData = additionalData;
}
const previousScore = await this.getPreviousScore(playerId, leaderboard.id + "", score.timestamp);
if (previousScore !== undefined) {
score.previousScore = previousScore;
}
scores.push({
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 leaderboardId the leaderboard to get the previous scores on
* @param page the page to get
*/
public static async getPreviousScores(
public static async getScoreHistory(
playerId: string,
leaderboardId: string,
page: number
@ -533,6 +541,10 @@ export class ScoreService {
if (additionalData !== undefined) {
score.additionalData = additionalData;
}
const previousScore = await this.getPreviousScore(playerId, leaderboardId, score.timestamp);
if (previousScore !== undefined) {
score.previousScore = previousScore;
}
toReturn.push({
score: score as unknown as ScoreSaberScore,
@ -544,4 +556,55 @@ export class ScoreService {
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;
}
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,14 +1,19 @@
import { ScoreBadgeProps } from "@/components/score/badges/badge-props";
import { formatNumberWithCommas } from "@ssr/common/utils/number-utils";
import { Change } from "@/common/change";
import Tooltip from "@/components/tooltip";
export function ScoreScoreBadge({ score }: ScoreBadgeProps) {
const scoreImprovement = score.additionalData?.scoreImprovement;
const previousScore = score.previousScore;
return (
<div className="flex flex-col items-center justify-center">
<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>
);
}