2024-10-16 07:31:52 +01:00
import ScoreSaberPlayerScoreToken from "@ssr/common/types/token/scoresaber/score-saber-player-score-token" ;
2024-10-18 07:56:39 +01:00
import { formatNumberWithCommas , formatPp } from "@ssr/common/utils/number-utils" ;
2024-10-16 08:21:27 +01:00
import { isProduction } from "@ssr/common/utils/utils" ;
2024-10-17 15:30:14 +01:00
import { Metadata } from "@ssr/common/types/metadata" ;
import { NotFoundError } from "elysia" ;
import BeatSaverService from "./beatsaver.service" ;
2024-10-19 14:11:43 +01:00
import ScoreSaberLeaderboard , {
getScoreSaberLeaderboardFromToken ,
} from "@ssr/common/leaderboard/impl/scoresaber-leaderboard" ;
2024-10-17 15:30:14 +01:00
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" ;
2024-10-23 15:33:25 +01:00
import { BeatSaverMap } from "@ssr/common/model/beatsaver/map" ;
2024-10-17 15:30:14 +01:00
import { PlayerScore } from "@ssr/common/score/player-score" ;
import LeaderboardScoresResponse from "@ssr/common/response/leaderboard-scores-response" ;
import PlayerScoresResponse from "@ssr/common/response/player-scores-response" ;
2024-10-17 18:29:30 +01:00
import { DiscordChannels , logToChannel } from "../bot/bot" ;
import { EmbedBuilder } from "discord.js" ;
2024-10-18 10:32:13 +01:00
import { Config } from "@ssr/common/config" ;
2024-10-19 04:10:44 +01:00
import { SSRCache } from "@ssr/common/cache" ;
import { fetchWithCache } from "../common/cache.util" ;
2024-10-22 15:59:41 +01:00
import { PlayerDocument , PlayerModel } from "@ssr/common/model/player" ;
2024-10-23 17:44:55 +01:00
import { BeatLeaderScoreToken } from "@ssr/common/types/token/beatleader/score/score" ;
2024-10-22 17:30:14 +01:00
import {
AdditionalScoreData ,
AdditionalScoreDataModel ,
2024-10-22 17:48:32 +01:00
} from "@ssr/common/model/additional-score-data/additional-score-data" ;
2024-10-23 17:44:55 +01:00
import { BeatLeaderScoreImprovementToken } from "@ssr/common/types/token/beatleader/score/score-improvement" ;
2024-10-23 20:20:57 +01:00
import { getScoreSaberScoreFromToken , ScoreSaberScoreModel } from "@ssr/common/model/score/impl/scoresaber-score" ;
import { ScoreType } from "@ssr/common/model/score/score" ;
2024-10-19 04:10:44 +01:00
const playerScoresCache = new SSRCache ( {
ttl : 1000 * 60 , // 1 minute
} ) ;
const leaderboardScoresCache = new SSRCache ( {
ttl : 1000 * 60 , // 1 minute
} ) ;
2024-10-16 07:31:52 +01:00
export class ScoreService {
2024-10-17 03:08:27 +01:00
/ * *
* Notifies the number one score in Discord .
*
* @param playerScore the score to notify
* /
2024-10-16 07:31:52 +01:00
public static async notifyNumberOne ( playerScore : ScoreSaberPlayerScoreToken ) {
2024-10-16 08:21:27 +01:00
// Only notify in production
if ( ! isProduction ( ) ) {
return ;
}
2024-10-18 07:56:39 +01:00
const { score : scoreToken , leaderboard : leaderboardToken } = playerScore ;
const leaderboard = getScoreSaberLeaderboardFromToken ( leaderboardToken ) ;
2024-10-23 20:52:57 +01:00
const score = getScoreSaberScoreFromToken ( scoreToken , leaderboard , scoreToken . leaderboardPlayerInfo . id ) ;
2024-10-18 07:56:39 +01:00
const playerInfo = score . playerInfo ;
2024-10-16 07:31:52 +01:00
// Not ranked
if ( leaderboard . stars <= 0 ) {
return ;
}
// Not #1 rank
if ( score . rank !== 1 ) {
return ;
}
2024-10-19 09:58:30 +01:00
const player = await scoresaberService . lookupPlayer ( playerInfo . id ) ;
if ( ! player ) {
return ;
}
2024-10-17 19:02:32 +01:00
await logToChannel (
2024-10-17 18:29:30 +01:00
DiscordChannels . numberOneFeed ,
new EmbedBuilder ( )
2024-10-18 07:56:39 +01:00
. setTitle ( ` ${ player . name } just set a #1! ` )
2024-10-17 18:29:30 +01:00
. setDescription (
2024-10-18 10:32:13 +01:00
[
` ${ leaderboard . songName } ${ leaderboard . songSubName } ( ${ leaderboard . difficulty . difficulty } ${ leaderboard . stars . toFixed ( 2 ) } ★) ` ,
2024-10-19 11:17:08 +01:00
` [[Player]]( ${ Config . websiteUrl } /player/ ${ player . id } ) [[Leaderboard]]( ${ Config . websiteUrl } /leaderboard/ ${ leaderboard . id } ) ` ,
2024-10-18 10:32:13 +01:00
] . join ( "\n" )
2024-10-17 18:29:30 +01:00
)
2024-10-18 07:56:39 +01:00
. addFields ( [
{
name : "Accuracy" ,
value : ` ${ score . accuracy . toFixed ( 2 ) } % ` ,
inline : true ,
} ,
{
name : "PP" ,
2024-10-18 10:33:11 +01:00
value : ` ${ formatPp ( score . pp ) } pp ` ,
2024-10-18 07:56:39 +01:00
inline : true ,
} ,
{
name : "Player Rank" ,
2024-10-18 10:33:11 +01:00
value : ` # ${ formatNumberWithCommas ( player . rank ) } ` ,
2024-10-18 07:56:39 +01:00
inline : true ,
} ,
{
name : "Misses" ,
value : formatNumberWithCommas ( score . missedNotes ) ,
inline : true ,
} ,
{
name : "Bad Cuts" ,
value : formatNumberWithCommas ( score . badCuts ) ,
inline : true ,
} ,
{
name : "Max Combo" ,
value : formatNumberWithCommas ( score . maxCombo ) ,
inline : true ,
} ,
] )
. setThumbnail ( leaderboard . songArt )
. setTimestamp ( score . timestamp )
2024-10-17 18:29:30 +01:00
. setColor ( "#00ff00" )
) ;
2024-10-16 07:31:52 +01:00
}
2024-10-17 15:30:14 +01:00
2024-10-22 15:59:41 +01:00
/ * *
* Tracks ScoreSaber score .
*
* @param score the score to track
* @param leaderboard the leaderboard to track
* /
2024-10-23 20:52:57 +01:00
public static async trackScoreSaberScore ( { score , leaderboard : leaderboardToken } : ScoreSaberPlayerScoreToken ) {
const leaderboard = getScoreSaberLeaderboardFromToken ( leaderboardToken ) ;
2024-10-22 15:59:41 +01:00
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 ( ) ;
2024-10-23 20:20:57 +01:00
const scoreToken = getScoreSaberScoreFromToken ( score , leaderboard , playerId ) ;
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
delete scoreToken . playerInfo ;
2024-10-23 20:47:12 +01:00
// Check if the score already exists
if (
await ScoreSaberScoreModel . exists ( {
playerId : playerId ,
leaderboardId : leaderboard.id ,
score : scoreToken.score ,
2024-10-23 20:52:57 +01:00
difficulty : leaderboard.difficulty.difficulty ,
characteristic : leaderboard.difficulty.characteristic ,
2024-10-23 20:47:12 +01:00
} )
) {
console . log (
` Score already exists for " ${ playerName } "( ${ playerId } ), scoreId= ${ scoreToken . scoreId } , score= ${ scoreToken . score } `
) ;
return ;
}
2024-10-23 20:20:57 +01:00
await ScoreSaberScoreModel . create ( scoreToken ) ;
2024-10-22 15:59:41 +01:00
console . log (
2024-10-23 20:20:57 +01:00
` Tracked score and updated scores set statistic for " ${ playerName } "( ${ playerId } ), scores today: ${ scores . rankedScores } ranked, ${ scores . unrankedScores } unranked `
2024-10-22 15:59:41 +01:00
) ;
}
/ * *
* 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 ;
}
2024-10-22 17:30:14 +01:00
// The score has already been tracked, so ignore it.
if (
( await this . getAdditionalScoreData (
playerId ,
leaderboard . song . hash ,
leaderboard . difficulty . difficultyName ,
score . baseScore
) ) !== undefined
) {
return ;
}
2024-10-22 17:48:32 +01:00
const getMisses = ( score : BeatLeaderScoreToken | BeatLeaderScoreImprovementToken ) = > {
return score . missedNotes + score . badCuts + score . bombCuts ;
} ;
2024-10-22 15:59:41 +01:00
const difficulty = leaderboard . difficulty ;
2024-10-23 15:33:25 +01:00
const difficultyKey = ` ${ difficulty . difficultyName } - ${ difficulty . modeName } ` ;
2024-10-22 17:30:14 +01:00
const rawScoreImprovement = score . scoreImprovement ;
const data = {
2024-10-22 15:59:41 +01:00
playerId : playerId ,
2024-10-22 17:30:14 +01:00
songHash : leaderboard.song.hash.toUpperCase ( ) ,
2024-10-22 15:59:41 +01:00
songDifficulty : difficultyKey ,
songScore : score.baseScore ,
2024-10-22 18:10:33 +01:00
scoreId : score.id ,
leaderboardId : leaderboard.id ,
2024-10-22 17:30:14 +01:00
misses : {
2024-10-22 17:48:32 +01:00
misses : getMisses ( score ) ,
2024-10-22 17:30:14 +01:00
missedNotes : score.missedNotes ,
bombCuts : score.bombCuts ,
badCuts : score.badCuts ,
wallsHit : score.wallsHit ,
} ,
2024-10-22 15:59:41 +01:00
pauses : score.pauses ,
fcAccuracy : score.fcAccuracy * 100 ,
2024-10-22 17:30:14 +01:00
fullCombo : score.fullCombo ,
2024-10-22 15:59:41 +01:00
handAccuracy : {
left : score.accLeft ,
right : score.accRight ,
} ,
2024-10-22 19:10:18 +01:00
timestamp : new Date ( Number ( score . timeset ) * 1000 ) ,
2024-10-22 17:30:14 +01:00
} as AdditionalScoreData ;
2024-10-22 18:55:47 +01:00
if ( rawScoreImprovement && rawScoreImprovement . score > 0 ) {
2024-10-22 17:30:14 +01:00
data . scoreImprovement = {
score : rawScoreImprovement.score ,
misses : {
2024-10-22 17:48:32 +01:00
misses : getMisses ( rawScoreImprovement ) ,
2024-10-22 17:30:14 +01:00
missedNotes : rawScoreImprovement.missedNotes ,
bombCuts : rawScoreImprovement.bombCuts ,
badCuts : rawScoreImprovement.badCuts ,
wallsHit : rawScoreImprovement.wallsHit ,
} ,
accuracy : rawScoreImprovement.accuracy * 100 ,
handAccuracy : {
left : rawScoreImprovement.accLeft ,
right : rawScoreImprovement.accRight ,
} ,
} ;
}
await AdditionalScoreDataModel . create ( data ) ;
2024-10-22 15:59:41 +01:00
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 < AdditionalScoreData | undefined > {
const additionalData = await AdditionalScoreDataModel . findOne ( {
playerId : playerId ,
2024-10-22 17:30:14 +01:00
songHash : songHash.toUpperCase ( ) ,
2024-10-22 15:59:41 +01:00
songDifficulty : songDifficulty ,
songScore : songScore ,
} ) ;
if ( ! additionalData ) {
return undefined ;
}
return additionalData . toObject ( ) ;
}
2024-10-17 15:30:14 +01:00
/ * *
* Gets scores for a player .
*
* @param leaderboardName the leaderboard to get the scores from
2024-10-23 20:20:57 +01:00
* @param playerId the players id
2024-10-17 15:30:14 +01:00
* @param page the page to get
* @param sort the sort to use
* @param search the search to use
* @returns the scores
* /
public static async getPlayerScores (
leaderboardName : Leaderboards ,
2024-10-23 20:20:57 +01:00
playerId : string ,
2024-10-17 15:30:14 +01:00
page : number ,
sort : string ,
search? : string
2024-10-19 04:10:44 +01:00
) : Promise < PlayerScoresResponse < unknown , unknown > | undefined > {
return fetchWithCache (
playerScoresCache ,
2024-10-23 20:20:57 +01:00
` player-scores- ${ leaderboardName } - ${ playerId } - ${ page } - ${ sort } - ${ search } ` ,
2024-10-19 04:10:44 +01:00
async ( ) = > {
const scores : PlayerScore < unknown , unknown > [ ] | undefined = [ ] ;
let metadata : Metadata = new Metadata ( 0 , 0 , 0 , 0 ) ; // Default values
switch ( leaderboardName ) {
case "scoresaber" : {
const leaderboardScores = await scoresaberService . lookupPlayerScores ( {
2024-10-23 20:20:57 +01:00
playerId : playerId ,
2024-10-19 04:10:44 +01:00
page : page ,
sort : sort as ScoreSort ,
search : search ,
} ) ;
if ( leaderboardScores == undefined ) {
break ;
}
metadata = new Metadata (
Math . ceil ( leaderboardScores . metadata . total / leaderboardScores . metadata . itemsPerPage ) ,
leaderboardScores . metadata . total ,
leaderboardScores . metadata . page ,
leaderboardScores . metadata . itemsPerPage
) ;
for ( const token of leaderboardScores . playerScores ) {
2024-10-23 20:52:57 +01:00
const leaderboard = getScoreSaberLeaderboardFromToken ( token . leaderboard ) ;
if ( leaderboard == undefined ) {
2024-10-19 04:10:44 +01:00
continue ;
}
2024-10-23 20:20:57 +01:00
2024-10-23 20:52:57 +01:00
const score = getScoreSaberScoreFromToken ( token . score , leaderboard , playerId ) ;
if ( score == undefined ) {
2024-10-19 04:10:44 +01:00
continue ;
}
2024-10-22 15:59:41 +01:00
const additionalData = await this . getAdditionalScoreData (
2024-10-23 20:20:57 +01:00
playerId ,
2024-10-23 15:33:25 +01:00
leaderboard . songHash ,
` ${ leaderboard . difficulty . difficulty } - ${ leaderboard . difficulty . characteristic } ` ,
2024-10-22 15:59:41 +01:00
score . score
) ;
if ( additionalData !== undefined ) {
score . additionalData = additionalData ;
}
2024-10-19 04:10:44 +01:00
scores . push ( {
score : score ,
2024-10-23 15:33:25 +01:00
leaderboard : leaderboard ,
beatSaver : await BeatSaverService . getMap ( leaderboard . songHash ) ,
2024-10-19 04:10:44 +01:00
} ) ;
}
break ;
2024-10-17 15:30:14 +01:00
}
2024-10-19 04:10:44 +01:00
default : {
throw new NotFoundError ( ` Leaderboard " ${ leaderboardName } " not found ` ) ;
2024-10-17 15:30:14 +01:00
}
}
2024-10-19 04:10:44 +01:00
return {
scores : scores ,
metadata : metadata ,
} ;
}
) ;
2024-10-17 15:30:14 +01:00
}
/ * *
* Gets scores for a leaderboard .
*
* @param leaderboardName the leaderboard to get the scores from
2024-10-23 20:20:57 +01:00
* @param leaderboardId the leaderboard id
2024-10-17 15:30:14 +01:00
* @param page the page to get
* @returns the scores
* /
public static async getLeaderboardScores (
leaderboardName : Leaderboards ,
2024-10-23 20:20:57 +01:00
leaderboardId : string ,
2024-10-17 15:30:14 +01:00
page : number
2024-10-19 04:10:44 +01:00
) : Promise < LeaderboardScoresResponse < unknown , unknown > | undefined > {
2024-10-23 20:20:57 +01:00
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
2024-10-17 15:30:14 +01:00
2024-10-23 20:20:57 +01:00
switch ( leaderboardName ) {
case "scoresaber" : {
const leaderboardResponse = await LeaderboardService . getLeaderboard < ScoreSaberLeaderboard > (
leaderboardName ,
leaderboardId
) ;
if ( leaderboardResponse == undefined ) {
throw new NotFoundError ( ` Leaderboard " ${ leaderboardName } " not found ` ) ;
}
leaderboard = leaderboardResponse . leaderboard ;
beatSaverMap = leaderboardResponse . beatsaver ;
2024-10-17 16:24:10 +01:00
2024-10-23 20:20:57 +01:00
const leaderboardScores = await scoresaberService . lookupLeaderboardScores ( leaderboardId , page ) ;
if ( leaderboardScores == undefined ) {
break ;
2024-10-19 04:10:44 +01:00
}
2024-10-17 15:30:14 +01:00
2024-10-23 20:20:57 +01:00
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 ;
}
default : {
throw new NotFoundError ( ` Leaderboard " ${ leaderboardName } " not found ` ) ;
}
2024-10-19 04:10:44 +01:00
}
2024-10-17 15:30:14 +01:00
2024-10-23 20:20:57 +01:00
return {
scores : scores ,
leaderboard : leaderboard ,
beatSaver : beatSaverMap ,
metadata : metadata ,
} ;
}
) ;
2024-10-17 15:30:14 +01:00
}
2024-10-16 07:31:52 +01:00
}