diff --git a/src/cleaners/scores.ts b/src/cleaners/scores.ts index 92a98a2b..21e0962b 100644 --- a/src/cleaners/scores.ts +++ b/src/cleaners/scores.ts @@ -1,5 +1,6 @@ import { accuracyCalculator, getPerformanceResults, getRetryCount, hitValueCalculator } from "@utils/osu"; import { grades, rulesets } from "@utils/emotes"; +import { insertData } from "@utils/database"; import type { UserScore, Beatmap, LeaderboardScores, Mode, PlayStatistics, ScoresInfo, Score, UserBestScore } from "@type/osu"; import type { ISOTimestamp } from "osu-web.js"; @@ -70,6 +71,100 @@ export async function getScore({ scores, beatmap: map_, index, mode, mapData }: // Throw an error if performance doesn't exist. // This can only mean one thing, and it's because the map couldn't be downloaded for some reason. if (!performance) throw new Error("Scores.ts panicked!", { cause: "`performanece` doesn't exist, presumably because the map couldn't be downloaded." }); + const { fc, current, difficultyAttrs, perfect, mapValues } = performance; + const { state } = current; + + if (play.passed && "score" in play) { + insertData({ + id: play.id, + table: "osu_scores", + data: [ + { + name: "user_id", + value: play.user_id + }, + { + name: "map_id", + value: beatmap.id + }, + { + name: "gamemode", + value: mode + }, + { + name: "mods", + value: play.mods.join("") + }, + { + name: "score", + value: totalScore + }, + { + name: "accuracy", + value: play.accuracy + }, + { + name: "max_combo", + value: play.max_combo + }, + { + name: "grade", + value: play.rank + }, + { + name: "count_50", + value: state?.n50 ?? 0 + }, + { + name: "count_100", + value: state?.n100 ?? 0 + }, + { + name: "count_300", + value: state?.n300 ?? 0 + }, + { + name: "count_geki", + value: state?.nGeki ?? 0 + }, + { + name: "count_katu", + value: state?.nKatu ?? 0 + }, + { + name: "count_miss", + value: state?.misses ?? 0 + }, + { + name: "map_state", + value: beatmap.status + }, + { + name: "ended_at", + value: play.created_at + } + ] + }, true); + + insertData({ + table: "osu_scores_pp", + id: play.id, + data: [ + { + name: "pp", + value: current.pp + }, + { + name: "pp_fc", + value: fc.pp + }, + { + name: "pp_perfect", + value: perfect.pp + } + ] + }, true); + } // We won't be needing this anymore, since osu! API now returns _null_ if the statistic key is not a part of the gamemode! // I'm not deleting the code in case I need it in the future if they decide to revert. @@ -98,7 +193,7 @@ export async function getScore({ scores, beatmap: map_, index, mode, mapData }: const hitValues = hitValueCalculator(mode, scoreStatistics); const playMaxCombo = play.max_combo; - const { maxCombo } = performance.current.difficulty; + const { maxCombo } = current.difficulty; const isFc = scoreStatistics.count_miss === 0 && playMaxCombo + 7 >= maxCombo; // set value to null because we won't always need it. @@ -117,7 +212,6 @@ export async function getScore({ scores, beatmap: map_, index, mode, mapData }: } = null; if (!isFc) { - const { fc } = performance; fcStatistics = { count_300: fc.state?.n300, count_100: fc.state?.n100, @@ -129,14 +223,14 @@ export async function getScore({ scores, beatmap: map_, index, mode, mapData }: fcAccuracy = accuracyCalculator(mode, fcStatistics); ifFcHanami = `FC: **${fc.pp.toFixed(2).toLocaleString()}pp** for **${fcAccuracy.toFixed(2)}%**`; - ifFcBathbot = `**${fc.pp.toFixed(2).toLocaleString()}**/${performance.perfect.pp.toFixed(2).toLocaleString()}PP`; + ifFcBathbot = `**${fc.pp.toFixed(2).toLocaleString()}**/${perfect.pp.toFixed(2).toLocaleString()}PP`; ifFcOwo = `(${fc.pp.toFixed(2).toLocaleString()}PP for ${fcAccuracy.toFixed(2)}% FC)`; } const fcHitValues = hitValueCalculator(mode, fcStatistics); // Get beatmap's drain length - const drainLengthInSeconds = beatmap.total_length / performance.difficultyAttrs.clockRate; + const drainLengthInSeconds = beatmap.total_length / difficultyAttrs.clockRate; const drainMinutes = Math.floor(drainLengthInSeconds / 60); // I thought Math.ceil would do a better job here since if the seconds is gonna be like, 40.88, @@ -144,7 +238,7 @@ export async function getScore({ scores, beatmap: map_, index, mode, mapData }: const drainSeconds = Math.ceil(drainLengthInSeconds % 60); const objectsHit = (scoreStatistics.count_300 ?? 0) + (scoreStatistics.count_100 ?? 0) + (scoreStatistics.count_50 ?? 0) + scoreStatistics.count_miss; - const objects = performance.mapValues.nObjects; + const objects = mapValues.nObjects; const percentageNum = objectsHit / objects * 100; const beatmapStatus = beatmapset.status; @@ -174,9 +268,9 @@ export async function getScore({ scores, beatmap: map_, index, mode, mapData }: mapAuthor: beatmapset.creator, mapStatus: beatmapStatus.charAt(0).toUpperCase() + beatmapStatus.slice(1), drainLength: `${drainMinutes}:${drainSeconds < 10 ? `0${drainSeconds}` : drainSeconds}`, - stars: `${performance.current.difficulty.stars.toFixed(2).toLocaleString()}★`, + stars: `${current.difficulty.stars.toFixed(2).toLocaleString()}★`, rulesetEmote: rulesets[mode], - ppFormatted: `**${performance.current.pp.toFixed(2).toLocaleString()}**/${performance.perfect.pp.toFixed(2).toLocaleLowerCase()}pp`, + ppFormatted: `**${current.pp.toFixed(2).toLocaleString()}**/${perfect.pp.toFixed(2).toLocaleLowerCase()}pp`, playSubmitted: ``, ifFcHanami, ifFcBathbot, diff --git a/src/embed-builders/plays.ts b/src/embed-builders/plays.ts index 8ebde8e1..a0fe9e9a 100644 --- a/src/embed-builders/plays.ts +++ b/src/embed-builders/plays.ts @@ -80,7 +80,7 @@ async function getSinglePlay({ mode, index, plays, profile, authorDb, isMultiple const play = await getScore({ scores: plays, index, mode }); const { mapValues, difficultyAttrs, current } = play.performance; - const bpm = difficultyAttrs.clockRate * mapValues.bpm + const bpm = difficultyAttrs.clockRate * mapValues.bpm; if (embedType === EmbedScoreType.Hanami) { const author = { @@ -105,7 +105,8 @@ async function getSinglePlay({ mode, index, plays, profile, authorDb, isMultiple fields[0].value += line3; const beatmapInfoField = [ `**BPM:** \`${bpm.toFixed().toLocaleString()}\` ${SPACE} **Length:** \`${play.drainLength}\``, - `**AR:** \`${difficultyAttrs.ar.toFixed(1)}\` ${SPACE} **OD:** \`${difficultyAttrs.od.toFixed(1)}\` ${SPACE} **CS:** \`${difficultyAttrs.cs.toFixed(1)}\` ${SPACE} **HP:** \`${difficultyAttrs.hp.toFixed(1)}\`` + `**AR:** \`${difficultyAttrs.ar.toFixed(1)}\` ${SPACE} **OD:** \`${difficultyAttrs.od + .toFixed(1)}\` ${SPACE} **CS:** \`${difficultyAttrs.cs.toFixed(1)}\` ${SPACE} **HP:** \`${difficultyAttrs.hp.toFixed(1)}\`` ]; fields.push({ name: "Beatmap Info:", diff --git a/src/types/database.ts b/src/types/database.ts index 5b61ffe2..4c37642a 100644 --- a/src/types/database.ts +++ b/src/types/database.ts @@ -1,3 +1,5 @@ +import type { Mode } from "./osu"; + export enum EmbedScoreType { Hanami = "hanami", Bathbot = "bathbot", @@ -30,6 +32,33 @@ export interface DatabaseCommands { count: string | null; } +export interface DatabaseScores { + id: number; + user_id: number; + map_id: number; + gamemode: Mode; + mods: string; + score: number; + accuracy: number; + max_combo: number; + grade: string; + count_50: number; + count_100: number; + count_300: number; + count_miss: number; + count_geki: number; + count_katu: number; + map_state: "ranked" | "graveyard" | "wip" | "pending" | "approved" | "qualified" | "loved"; + ended_at: string; +} + +export interface DatabaseScoresPp { + id: number; + pp: number; + pp_fc: number; + pp_perfect: number; +} + export enum ScoreEmbed { Maximized = 1, Minimized = 0 diff --git a/src/utils/database.ts b/src/utils/database.ts index 0e98a81b..4d36060f 100644 --- a/src/utils/database.ts +++ b/src/utils/database.ts @@ -1,5 +1,5 @@ import db from "../data.db" with { type: "sqlite" }; -import type { DatabaseMap, DatabaseGuild, DatabaseUser, DatabaseCommands } from "@type/database"; +import type { DatabaseMap, DatabaseGuild, DatabaseUser, DatabaseCommands, DatabaseScores, DatabaseScoresPp } from "@type/database"; export function query(str: string): unknown { return db.prepare(str).all(); @@ -33,23 +33,31 @@ export function getRowCount(table: string): number { } export function getMap(id: string | number): DatabaseMap | null { - return db.prepare("SELECT * FROM maps WHERE id = ?").get(id) as DatabaseMap; + return db.prepare("SELECT * FROM maps WHERE id = ?").get(id) as DatabaseMap | null; } export function getCommand(id: string | number): DatabaseCommands | null { - return db.prepare("SELECT * FROM commands WHERE id = ?").get(id) as DatabaseCommands; + return db.prepare("SELECT * FROM commands WHERE id = ?").get(id) as DatabaseCommands | null; } -export function insertData({ table, id, data }: { table: string, id: string | number, data: Array<{ name: string, value: string | number | null }> }): void { +export function getScores(id: string | number): DatabaseScores | null { + return db.prepare("SELECT * FROM commands WHERE id = ?").get(id) as DatabaseScores | null; +} + +export function getScoresPp(id: string | number): DatabaseScoresPp | null { + return db.prepare("SELECT * FROM commands WHERE id = ?").get(id) as DatabaseScoresPp | null; +} + +export function insertData({ table, id, data }: { table: string, id: string | number, data: Array<{ name: string, value: string | number | boolean | null }> }, ignore?: boolean): void { const setClause = data.map((item) => `${item.name} = ?`).join(", "); - const values: Array = data.map((item) => item.value); + const values: Array = data.map((item) => item.value); const existingRow = db.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(id); if (!existingRow) { const fields: Array = data.map((item) => item.name); const placeholders = fields.map(() => "?").join(", "); - db.prepare(`INSERT OR REPLACE INTO ${table} (id, ${fields.join(", ")}) values (?, ${placeholders});`) + db.prepare(`INSERT OR ${ignore ? "IGNORE" : "REPLACE"} INTO ${table} (id, ${fields.join(", ")}) values (?, ${placeholders});`) .run(id, ...values); } diff --git a/src/utils/initalize.ts b/src/utils/initalize.ts index 158c0d27..f78b55e9 100644 --- a/src/utils/initalize.ts +++ b/src/utils/initalize.ts @@ -174,12 +174,43 @@ interface Columns { } export function initializeDatabase(): void { - const tables = [ + const tables: Array<{ name: string, columns: Array }> = [ { name: "users", columns: ["id TEXT PRIMARY KEY", "banchoId TEXT", "score_embeds INTEGER", "mode TEXT", "embed_type TEXT"] }, { name: "servers", columns: ["id TEXT PRIMARY KEY", "name TEXT", "owner_id TEXT", "joined_at INTEGER", "prefixes TEXT"] }, { name: "maps", columns: ["id TEXT PRIMARY KEY", "data TEXT"] }, - { name: "commands", columns: ["id TEXT PRIMARY KEY", "count TEXT"] }, - { name: "commands_slash", columns: ["id TEXT PRIMARY KEY", "count TEXT"] } + { name: "commands", columns: ["id TEXT PRIMARY KEY", "count INTEGER"] }, + { name: "commands_slash", columns: ["id TEXT PRIMARY KEY", "count INTEGER"] }, + { + name: "osu_scores", + columns: [ + "id INTEGER PRIMARY KEY", + "user_id INTEGER", + "map_id INTEGER", + "gamemode INTEGER", + "mods TEXT", + "score INTEGER", + "accuracy INTEGER", + "max_combo INTEGER", + "grade TEXT", + "count_50 INTEGER", + "count_100 INTEGER", + "count_300 INTEGER", + "count_miss INTEGER", + "count_geki INTEGER", + "count_katu INTEGER", + "map_state TEXT", + "ended_at TEXT" + ] + }, + { + name: "osu_scores_pp", + columns: [ + "id INTEGER PRIMARY KEY", + "pp INTEGER", + "pp_fc INTEGER", + "pp_perfect INTEGER" + ] + } ]; for (let i = 0; i < tables.length; i++) {