add beatleader data tracking!!!!!!!!!!!!!
Some checks failed
Deploy Backend / docker (ubuntu-latest) (push) Failing after 44s
Deploy Website / docker (ubuntu-latest) (push) Failing after 38s

This commit is contained in:
Lee
2024-10-22 15:59:41 +01:00
parent 074d4de123
commit fa2ba83c7a
36 changed files with 767 additions and 173 deletions

View File

@ -41,7 +41,7 @@ export function getScoreSaberLeaderboardFromToken(token: ScoreSaberLeaderboardTo
const difficulty: LeaderboardDifficulty = {
leaderboardId: token.difficulty.leaderboardId,
difficulty: getDifficultyFromScoreSaberDifficulty(token.difficulty.difficulty),
gameMode: token.difficulty.gameMode,
gameMode: token.difficulty.gameMode.replace("Solo", ""),
difficultyRaw: token.difficulty.difficultyRaw,
};
@ -66,7 +66,7 @@ export function getScoreSaberLeaderboardFromToken(token: ScoreSaberLeaderboardTo
return {
leaderboardId: difficulty.leaderboardId,
difficulty: getDifficultyFromScoreSaberDifficulty(difficulty.difficulty),
gameMode: difficulty.gameMode,
gameMode: difficulty.gameMode.replace("Solo", ""),
difficultyRaw: difficulty.difficultyRaw,
};
})

View File

@ -0,0 +1,95 @@
import { getModelForClass, modelOptions, prop, ReturnModelType, Severity } from "@typegoose/typegoose";
import { Document } from "mongoose";
/**
* The model for a BeatSaver map.
*/
@modelOptions({
options: { allowMixed: Severity.ALLOW },
schemaOptions: {
collection: "additional-score-data",
toObject: {
virtuals: true,
transform: function (_, ret) {
delete ret._id;
delete ret.playerId;
delete ret.songHash;
delete ret.songDifficulty;
delete ret.songScore;
delete ret.__v;
return ret;
},
},
},
})
export class AdditionalScoreData {
/**
* The of the player who set the score.
*/
@prop({ required: true, index: true })
public playerId!: string;
/**
* The hash of the song.
*/
@prop({ required: true, index: true })
public songHash!: string;
/**
* The difficulty the score was set on.
*/
@prop({ required: true, index: true })
public songDifficulty!: string;
/**
* The score of the play.
*/
@prop({ required: true, index: true })
public songScore!: number;
/**
* The amount of times a bomb was hit.
*/
@prop({ required: false })
public bombCuts!: number;
/**
* The amount of walls hit in the play.
*/
@prop({ required: false })
public wallsHit!: number;
/**
* The amount of pauses in the play.
*/
@prop({ required: false })
public pauses!: number;
/**
* The hand accuracy for each hand.
* @private
*/
@prop({ required: false })
public handAccuracy!: {
/**
* The left hand accuracy.
*/
left: number;
/**
* The right hand accuracy.
*/
right: number;
};
/**
* The full combo accuracy of the play.
*/
@prop({ required: true })
public fcAccuracy!: number;
}
export type AdditionalScoreDataDocument = AdditionalScoreData & Document;
export const AdditionalScoreDataModel: ReturnModelType<typeof AdditionalScoreData> =
getModelForClass(AdditionalScoreData);

View File

@ -35,7 +35,7 @@ export interface PlayerHistory {
};
/**
* The amount of scores set for this day.
* The player's scores stats.
*/
scores?: {
/**
@ -60,7 +60,7 @@ export interface PlayerHistory {
};
/**
* The player's accuracy.
* The player's accuracy stats.
*/
accuracy?: {
/**

View File

@ -1,5 +1,6 @@
import { Modifier } from "./modifier";
import { Leaderboards } from "../leaderboard";
import { AdditionalScoreData } from "../model/additional-score-data";
export default interface Score {
/**
@ -53,6 +54,11 @@ export default interface Score {
*/
readonly fullCombo: boolean;
/**
* The additional data for the score.
*/
additionalData?: AdditionalScoreData;
/**
* The time the score was set.
* @private

View File

@ -0,0 +1,30 @@
import { BeatLeaderModifierToken } from "./beatleader-modifiers-token";
import { BeatLeaderModifierRatingToken } from "./beatleader-modifier-rating-token";
export type BeatLeaderDifficultyToken = {
id: number;
value: number;
mode: number;
difficultyName: string;
modeName: string;
status: number;
modifierValues: BeatLeaderModifierToken;
modifiersRating: BeatLeaderModifierRatingToken;
nominatedTime: number;
qualifiedTime: number;
rankedTime: number;
stars: number;
predictedAcc: number;
passRating: number;
accRating: number;
techRating: number;
type: number;
njs: number;
nps: number;
notes: number;
bombs: number;
walls: number;
maxScore: number;
duration: number;
requirements: number;
};

View File

@ -0,0 +1,16 @@
import { BeatLeaderSongToken } from "./beatleader-song-token";
import { BeatLeaderDifficultyToken } from "./beatleader-difficulty-token";
export type BeatLeaderLeaderboardToken = {
id: string;
song: BeatLeaderSongToken;
difficulty: BeatLeaderDifficultyToken;
scores: null; // ??
changes: null; // ??
qualification: null; // ??
reweight: null; // ??
leaderboardGroup: null; // ??
plays: number;
clan: null; // ??
clanRankingContested: boolean;
};

View File

@ -0,0 +1,18 @@
export type BeatLeaderModifierRatingToken = {
id: number;
fsPredictedAcc: number;
fsPassRating: number;
fsAccRating: number;
fsTechRating: number;
fsStars: number;
ssPredictedAcc: number;
ssPassRating: number;
ssAccRating: number;
ssTechRating: number;
ssStars: number;
sfPredictedAcc: number;
sfPassRating: number;
sfAccRating: number;
sfTechRating: number;
sfStars: number;
};

View File

@ -0,0 +1,16 @@
export type BeatLeaderModifierToken = {
modifierId: number;
da: number;
fs: number;
sf: number;
ss: number;
gn: number;
na: number;
nb: number;
nf: number;
no: number;
pm: number;
sc: number;
sa: number;
op: number;
};

View File

@ -0,0 +1,10 @@
export type BeatLeaderPlayerToken = {
id: string;
country: string;
avatar: string;
pp: number;
rank: number;
countryRank: number;
name: string;
// todo: finish this
};

View File

@ -0,0 +1,19 @@
export type BeatLeaderScoreImprovementToken = {
id: number;
timeset: number;
score: number;
accuracy: number;
pp: number;
bonusPp: number;
rank: number;
accRight: number;
accLeft: number;
averageRankedAccuracy: number;
totalPp: number;
totalRank: number;
badCuts: number;
missedNotes: number;
bombCuts: number;
wallsHit: number;
pauses: number;
};

View File

@ -0,0 +1,8 @@
export type BeatLeaderScoreOffsetsToken = {
id: number;
frames: number;
notes: number;
walls: number;
heights: number;
pauses: number;
};

View File

@ -0,0 +1,52 @@
import { BeatLeaderLeaderboardToken } from "./beatleader-leaderboard-token";
import { BeatLeaderScoreImprovementToken } from "./beatleader-score-improvement-token";
import { BeatLeaderScoreOffsetsToken } from "./beatleader-score-offsets-token";
import { BeatLeaderPlayerToken } from "./beatleader-player-token";
export type BeatLeaderScoreToken = {
myScore: null; // ??
validContexts: number;
leaderboard: BeatLeaderLeaderboardToken;
contextExtensions: null; // ??
accLeft: number;
accRight: number;
id: number;
baseScore: number;
modifiedScore: number;
accuracy: number;
playerId: string;
pp: number;
bonusPp: number;
passPP: number;
accPP: number;
techPP: number;
rank: number;
country: string;
fcAccuracy: number;
fcPp: number;
weight: number;
replay: string;
modifiers: string;
badCuts: number;
missedNotes: number;
bombCuts: number;
wallsHit: number;
pauses: number;
fullCombo: boolean;
platform: string;
maxCombo: number;
maxStreak: number;
hmd: number;
controller: number;
leaderboardId: string;
timeset: string;
timepost: number;
replaysWatched: number;
playCount: number;
priority: number;
player: BeatLeaderPlayerToken; // ??
scoreImprovement: BeatLeaderScoreImprovementToken;
rankVoting: null; // ??
metadata: null; // ??
offsets: BeatLeaderScoreOffsetsToken;
};

View File

@ -0,0 +1,16 @@
export type BeatLeaderSongToken = {
id: string;
hash: string;
name: string;
subName: string;
author: string;
mapperId: string;
coverImage: string;
fullCoverImage: string;
downloadUrl: string;
bpm: number;
duration: number;
tags: string;
uploadTime: number;
difficulties: null; // ??
};

View File

@ -0,0 +1,30 @@
import { connectWebSocket, WebsocketCallbacks } from "./websocket";
import { BeatLeaderScoreToken } from "../types/token/beatleader/beatleader-score-token";
type BeatLeaderWebsocket = {
/**
* Invoked when a score message is received.
*
* @param score the received score data.
*/
onScore?: (score: BeatLeaderScoreToken) => void;
} & WebsocketCallbacks;
/**
* Connects to the BeatLeader websocket and handles incoming messages.
*
* @param onMessage the onMessage callback
* @param onScore the onScore callback
* @param onDisconnect the onDisconnect callback
*/
export function connectBeatLeaderWebsocket({ onMessage, onScore, onDisconnect }: BeatLeaderWebsocket) {
return connectWebSocket({
name: "BeatLeader",
url: "wss://sockets.api.beatleader.xyz/scores",
onMessage: (message: unknown) => {
onScore && onScore(message as BeatLeaderScoreToken);
onMessage && onMessage(message);
},
onDisconnect,
});
}

View File

@ -1,74 +1,38 @@
import WebSocket from "ws";
import { connectWebSocket, WebsocketCallbacks } from "./websocket";
import ScoreSaberPlayerScoreToken from "../types/token/scoresaber/score-saber-player-score-token";
import { ScoreSaberWebsocketMessageToken } from "../types/token/scoresaber/websocket/scoresaber-websocket-message";
type ScoresaberSocket = {
/**
* Invoked when a general message is received.
*
* @param message the received message.
*/
onMessage?: (message: unknown) => void;
type ScoresaberWebsocket = {
/**
* Invoked when a score message is received.
*
* @param score the received score data.
*/
onScore?: (score: ScoreSaberPlayerScoreToken) => void;
/**
* Invoked when the connection is closed.
*
* @param error the error that caused the connection to close
*/
onDisconnect?: (error?: WebSocket.ErrorEvent | WebSocket.CloseEvent) => void;
};
} & WebsocketCallbacks;
/**
* Connects to the ScoreSaber websocket and handles incoming messages.
* Connects to the Scoresaber websocket and handles incoming messages.
*
* @param onMessage the onMessage callback
* @param onScore the onScore callback
* @param onDisconnect the onDisconnect callback
*/
export function connectScoreSaberWebSocket({ onMessage, onScore, onDisconnect }: ScoresaberSocket) {
let websocket: WebSocket | null = null;
function connectWs() {
websocket = new WebSocket("wss://scoresaber.com/ws");
websocket.onopen = () => {
console.log("Connected to the ScoreSaber WebSocket!");
};
websocket.onerror = error => {
console.error("WebSocket Error:", error);
if (websocket) {
websocket.close(); // Close the connection on error
export function connectScoresaberWebsocket({ onMessage, onScore, onDisconnect }: ScoresaberWebsocket) {
return connectWebSocket({
name: "Scoresaber",
url: "wss://scoresaber.com/ws",
onMessage: (message: unknown) => {
const command = message as ScoreSaberWebsocketMessageToken;
if (typeof command !== "object" || command === null) {
return;
}
onDisconnect && onDisconnect(error);
};
websocket.onclose = event => {
console.log("Lost connection to the ScoreSaber WebSocket. Attempting to reconnect...");
onDisconnect && onDisconnect(event);
setTimeout(connectWs, 5000); // Reconnect after 5 seconds
};
websocket.onmessage = messageEvent => {
if (typeof messageEvent.data !== "string") return;
try {
const command = JSON.parse(messageEvent.data);
if (command.commandName === "score") {
onScore && onScore(command.commandData as ScoreSaberPlayerScoreToken);
} else {
onMessage && onMessage(command);
}
} catch (err) {
console.warn("Received invalid message:", messageEvent.data);
if (command.commandName === "score") {
onScore && onScore(command.commandData as ScoreSaberPlayerScoreToken);
} else {
onMessage && onMessage(command);
}
};
}
connectWs(); // Initiate the first connection
},
onDisconnect,
});
}

View File

@ -0,0 +1,89 @@
import WebSocket from "ws";
import { DiscordChannels, logToChannel } from "backend/src/bot/bot";
import { EmbedBuilder } from "discord.js";
export type WebsocketCallbacks = {
/**
* Invoked when a general message is received.
*
* @param message the received message.
*/
onMessage?: (message: unknown) => void;
/**
* Invoked when the connection is closed.
*
* @param error the error that caused the connection to close
*/
onDisconnect?: (error?: WebSocket.ErrorEvent | WebSocket.CloseEvent) => void;
};
type Websocket = {
/**
* The name of the service we're connecting to.
*/
name: string;
/**
* The url of the service we're connecting to.
*/
url: string;
} & WebsocketCallbacks;
/**
* Connects to the ScoreSaber websocket and handles incoming messages.
*/
export function connectWebSocket({ name, url, onMessage, onDisconnect }: Websocket) {
let websocket: WebSocket | null = null;
/**
* Logs to the backend logs channel.
*
* @param error the error to log
*/
async function log(error: WebSocket.ErrorEvent | WebSocket.CloseEvent) {
await logToChannel(
DiscordChannels.backendLogs,
new EmbedBuilder().setDescription(`${name} websocket disconnected: ${JSON.stringify(error)}`)
);
}
function connectWs() {
websocket = new WebSocket(url);
websocket.onopen = () => {
console.log(`Connected to the ${name} WebSocket!`);
};
websocket.onerror = event => {
console.error("WebSocket Error:", event);
if (websocket) {
websocket.close(); // Close the connection on error
}
onDisconnect && onDisconnect(event);
log(event);
};
websocket.onclose = event => {
console.log(`Lost connection to the ${name} WebSocket. Attempting to reconnect...`);
onDisconnect && onDisconnect(event);
log(event);
setTimeout(connectWs, 5000); // Reconnect after 5 seconds
};
websocket.onmessage = messageEvent => {
if (typeof messageEvent.data !== "string") return;
try {
const command = JSON.parse(messageEvent.data);
onMessage && onMessage(command);
} catch (err) {
console.warn(`Received invalid json message on ${name}:`, messageEvent.data);
}
};
}
connectWs(); // Initiate the first connection
}