import { PlayerDocument, PlayerModel } from "../model/player"; import { NotFoundError } from "../error/not-found-error"; import { getDaysAgoDate, getMidnightAlignedDate } from "@ssr/common/utils/time-utils"; 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"; export class PlayerService { /** * Get a player from the database. * * @param id the player to fetch * @param create if true, create the player if it doesn't exist * @returns the player * @throws NotFoundError if the player is not found */ public static async getPlayer(id: string, create: boolean = false): Promise { let player: PlayerDocument | null = await PlayerModel.findById(id); if (player === null) { // If create is on, create the player, otherwise return unknown player const playerToken = create ? await scoresaberService.lookupPlayer(id) : undefined; if (playerToken === undefined) { throw new NotFoundError(`Player "${id}" not found`); } console.log(`Creating player "${id}"...`); try { player = (await PlayerModel.create({ _id: id })) as PlayerDocument; player.trackedSince = new Date(); await this.seedPlayerHistory(player, playerToken); } catch (err) { const message = `Failed to create player document for "${id}"`; console.log(message, err); throw new InternalServerError(message); } } return player; } /** * Seeds the player's history using data from * the ScoreSaber API. * * @param player the player to seed * @param playerToken the SoreSaber player token */ public static async seedPlayerHistory(player: PlayerDocument, playerToken: ScoreSaberPlayerToken): Promise { // Loop through rankHistory in reverse, from current day backwards const playerRankHistory = playerToken.histories.split(",").map((value: string) => { return parseInt(value); }); playerRankHistory.push(playerToken.rank); let daysAgo = 1; // Start from yesterday for (let i = playerRankHistory.length - daysAgo - 1; i >= 0; i--) { const rank = playerRankHistory[i]; const date = getMidnightAlignedDate(getDaysAgoDate(daysAgo)); player.setStatisticHistory(date, { rank: rank, }); daysAgo += 1; // Increment daysAgo for each earlier rank } await player.save(); } /** * Tracks a players statistics * * @param foundPlayer the player to track * @param playerToken an optional player token */ public static async trackScoreSaberPlayer( foundPlayer: PlayerDocument, playerToken?: ScoreSaberPlayerToken ): Promise { const dateToday = getMidnightAlignedDate(new Date()); const player = playerToken ? playerToken : await scoresaberService.lookupPlayer(foundPlayer.id); if (player == undefined) { console.log(`Player "${foundPlayer.id}" not found on ScoreSaber`); return; } if (player.inactive) { console.log(`Player "${foundPlayer.id}" is inactive on ScoreSaber`); return; } // Seed the history with ScoreSaber data if no history exists if (foundPlayer.getDaysTracked() === 0) { await this.seedPlayerHistory(foundPlayer, player); } // Update current day's statistics let history = foundPlayer.getHistoryByDate(dateToday); if (history == undefined) { history = {}; // Initialize if history is not found } // Set the history data history.pp = player.pp; history.countryRank = player.countryRank; history.rank = player.rank; history.accuracy = { averageRankedAccuracy: player.scoreStats.averageRankedAccuracy, }; foundPlayer.setStatisticHistory(dateToday, history); foundPlayer.sortStatisticHistory(); foundPlayer.lastTracked = new Date(); await foundPlayer.save(); console.log(`Tracked player "${foundPlayer.id}"!`); } }