Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add new scores table #186

Merged
merged 6 commits into from
Apr 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 101 additions & 7 deletions src/cleaners/scores.ts
Original file line number Diff line number Diff line change
@@ -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";

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand All @@ -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,
Expand All @@ -129,22 +223,22 @@ 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,
// Instead of rounding it down to 40, it would make more sense to round it to 41.
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;
Expand Down Expand Up @@ -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: `<t:${new Date(createdAt).getTime() / 1000}:R>`,
ifFcHanami,
ifFcBathbot,
Expand Down
5 changes: 3 additions & 2 deletions src/embed-builders/plays.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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:",
Expand Down
29 changes: 29 additions & 0 deletions src/types/database.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { Mode } from "./osu";

export enum EmbedScoreType {
Hanami = "hanami",
Bathbot = "bathbot",
Expand Down Expand Up @@ -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
Expand Down
20 changes: 14 additions & 6 deletions src/utils/database.ts
Original file line number Diff line number Diff line change
@@ -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();
Expand Down Expand Up @@ -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<string | number | null> = data.map((item) => item.value);
const values: Array<string | number | boolean | null> = data.map((item) => item.value);

const existingRow = db.prepare(`SELECT * FROM ${table} WHERE id = ?`).get(id);
if (!existingRow) {
const fields: Array<string> = 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);
}

Expand Down
37 changes: 34 additions & 3 deletions src/utils/initalize.ts
Original file line number Diff line number Diff line change
Expand Up @@ -174,12 +174,43 @@ interface Columns {
}

export function initializeDatabase(): void {
const tables = [
const tables: Array<{ name: string, columns: Array<string> }> = [
{ 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++) {
Expand Down
Loading