From 11f32ccf046ba35196361575638a17d9eef8fe3a Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Wed, 6 Mar 2024 00:01:38 +0100 Subject: [PATCH] osu: Implement map length on server ranks --- README.md | 9 + ...20dfd7f9d3df62a35ba63c61efc5de70e750d.json | 12 -- ...58ec9de4802d90dbb2ab48de32c1a4ada601.json} | 12 +- ...6c0e59d61596cb609c4bb952edc2d64cec868.json | 12 ++ ...6b1a1cd2aeb89517b7ee09e7e6f8d6e0cd79.json} | 12 +- ...a90ec3137398e83b3d0c626209306804399a.json} | 12 +- ...0305211858_osu_add_weighted_map_length.sql | 5 + youmubot-db-sql/src/models/osu_user.rs | 23 ++- youmubot-osu/src/discord/announcer.rs | 32 ++++ youmubot-osu/src/discord/db.rs | 3 + youmubot-osu/src/discord/mod.rs | 1 + youmubot-osu/src/discord/server_rank.rs | 167 +++++++++++++----- 12 files changed, 229 insertions(+), 71 deletions(-) delete mode 100644 youmubot-db-sql/.sqlx/query-26a910506c0613936be169df13320dfd7f9d3df62a35ba63c61efc5de70e750d.json rename youmubot-db-sql/.sqlx/{query-5753fe315c9a55154d2d80e6d293dc8abffcf426b845624a42cd0bfefc75fb74.json => query-6ef67ca385287a4cef9fdd47bf4258ec9de4802d90dbb2ab48de32c1a4ada601.json} (81%) create mode 100644 youmubot-db-sql/.sqlx/query-a06efa1b12c2c7c9cf5b83bff796c0e59d61596cb609c4bb952edc2d64cec868.json rename youmubot-db-sql/.sqlx/{query-08f2568a69a14ae240a24264238d4abc7aea5eee67d6062d049f0d37031e4d7a.json => query-b098282e73cc6fd435330f6ecd446b1a1cd2aeb89517b7ee09e7e6f8d6e0cd79.json} (80%) rename youmubot-db-sql/.sqlx/{query-700ec95294d9a4f21e3d7ff53f15f5dc739bffe8fedc19e35cbb576b6dd2e948.json => query-df0aa5065268e59c68990ab46ab4a90ec3137398e83b3d0c626209306804399a.json} (80%) create mode 100644 youmubot-db-sql/migrations/20240305211858_osu_add_weighted_map_length.sql diff --git a/README.md b/README.md index 90c041e..356a591 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,15 @@ All PRs welcome. - `youmubot-core`: Core commands: admin, fun, community - `youmubot-osu`: osu!-related commands. +## Working with `sqlx` + +### Regenerate compiler information + +From within `./youmubot-db-sql` run +```bash +cargo sqlx prepare --database-url "sqlite:$(realpath ..)/youmubot.db" +``` + ## License Basically MIT. diff --git a/youmubot-db-sql/.sqlx/query-26a910506c0613936be169df13320dfd7f9d3df62a35ba63c61efc5de70e750d.json b/youmubot-db-sql/.sqlx/query-26a910506c0613936be169df13320dfd7f9d3df62a35ba63c61efc5de70e750d.json deleted file mode 100644 index cfd5f19..0000000 --- a/youmubot-db-sql/.sqlx/query-26a910506c0613936be169df13320dfd7f9d3df62a35ba63c61efc5de70e750d.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "SQLite", - "query": "INSERT\n INTO osu_users(user_id, username, id, last_update, pp_std, pp_taiko, pp_mania, pp_catch, failures)\n VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (user_id) WHERE id = ? DO UPDATE\n SET\n last_update = excluded.last_update,\n username = excluded.username,\n pp_std = excluded.pp_std,\n pp_taiko = excluded.pp_taiko,\n pp_mania = excluded.pp_mania,\n pp_catch = excluded.pp_catch,\n failures = excluded.failures\n ", - "describe": { - "columns": [], - "parameters": { - "Right": 10 - }, - "nullable": [] - }, - "hash": "26a910506c0613936be169df13320dfd7f9d3df62a35ba63c61efc5de70e750d" -} diff --git a/youmubot-db-sql/.sqlx/query-5753fe315c9a55154d2d80e6d293dc8abffcf426b845624a42cd0bfefc75fb74.json b/youmubot-db-sql/.sqlx/query-6ef67ca385287a4cef9fdd47bf4258ec9de4802d90dbb2ab48de32c1a4ada601.json similarity index 81% rename from youmubot-db-sql/.sqlx/query-5753fe315c9a55154d2d80e6d293dc8abffcf426b845624a42cd0bfefc75fb74.json rename to youmubot-db-sql/.sqlx/query-6ef67ca385287a4cef9fdd47bf4258ec9de4802d90dbb2ab48de32c1a4ada601.json index 7d74007..9935fbd 100644 --- a/youmubot-db-sql/.sqlx/query-5753fe315c9a55154d2d80e6d293dc8abffcf426b845624a42cd0bfefc75fb74.json +++ b/youmubot-db-sql/.sqlx/query-6ef67ca385287a4cef9fdd47bf4258ec9de4802d90dbb2ab48de32c1a4ada601.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\"\n FROM osu_users", + "query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\",\n std_weighted_map_length\n FROM osu_users", "describe": { "columns": [ { @@ -47,6 +47,11 @@ "name": "failures: u8", "ordinal": 8, "type_info": "Int64" + }, + { + "name": "std_weighted_map_length", + "ordinal": 9, + "type_info": "Float" } ], "parameters": { @@ -61,8 +66,9 @@ true, true, true, - false + false, + true ] }, - "hash": "5753fe315c9a55154d2d80e6d293dc8abffcf426b845624a42cd0bfefc75fb74" + "hash": "6ef67ca385287a4cef9fdd47bf4258ec9de4802d90dbb2ab48de32c1a4ada601" } diff --git a/youmubot-db-sql/.sqlx/query-a06efa1b12c2c7c9cf5b83bff796c0e59d61596cb609c4bb952edc2d64cec868.json b/youmubot-db-sql/.sqlx/query-a06efa1b12c2c7c9cf5b83bff796c0e59d61596cb609c4bb952edc2d64cec868.json new file mode 100644 index 0000000..f69c8f9 --- /dev/null +++ b/youmubot-db-sql/.sqlx/query-a06efa1b12c2c7c9cf5b83bff796c0e59d61596cb609c4bb952edc2d64cec868.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT\n INTO osu_users(user_id, username, id, last_update, pp_std, pp_taiko, pp_mania, pp_catch, failures, std_weighted_map_length)\n VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)\n ON CONFLICT (user_id) WHERE id = ? DO UPDATE\n SET\n last_update = excluded.last_update,\n username = excluded.username,\n pp_std = excluded.pp_std,\n pp_taiko = excluded.pp_taiko,\n pp_mania = excluded.pp_mania,\n pp_catch = excluded.pp_catch,\n failures = excluded.failures,\n std_weighted_map_length = excluded.std_weighted_map_length\n ", + "describe": { + "columns": [], + "parameters": { + "Right": 11 + }, + "nullable": [] + }, + "hash": "a06efa1b12c2c7c9cf5b83bff796c0e59d61596cb609c4bb952edc2d64cec868" +} diff --git a/youmubot-db-sql/.sqlx/query-08f2568a69a14ae240a24264238d4abc7aea5eee67d6062d049f0d37031e4d7a.json b/youmubot-db-sql/.sqlx/query-b098282e73cc6fd435330f6ecd446b1a1cd2aeb89517b7ee09e7e6f8d6e0cd79.json similarity index 80% rename from youmubot-db-sql/.sqlx/query-08f2568a69a14ae240a24264238d4abc7aea5eee67d6062d049f0d37031e4d7a.json rename to youmubot-db-sql/.sqlx/query-b098282e73cc6fd435330f6ecd446b1a1cd2aeb89517b7ee09e7e6f8d6e0cd79.json index 36adbd6..9d44a8a 100644 --- a/youmubot-db-sql/.sqlx/query-08f2568a69a14ae240a24264238d4abc7aea5eee67d6062d049f0d37031e4d7a.json +++ b/youmubot-db-sql/.sqlx/query-b098282e73cc6fd435330f6ecd446b1a1cd2aeb89517b7ee09e7e6f8d6e0cd79.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\"\n FROM osu_users WHERE id = ?", + "query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\",\n std_weighted_map_length\n FROM osu_users WHERE id = ?", "describe": { "columns": [ { @@ -47,6 +47,11 @@ "name": "failures: u8", "ordinal": 8, "type_info": "Int64" + }, + { + "name": "std_weighted_map_length", + "ordinal": 9, + "type_info": "Float" } ], "parameters": { @@ -61,8 +66,9 @@ true, true, true, - false + false, + true ] }, - "hash": "08f2568a69a14ae240a24264238d4abc7aea5eee67d6062d049f0d37031e4d7a" + "hash": "b098282e73cc6fd435330f6ecd446b1a1cd2aeb89517b7ee09e7e6f8d6e0cd79" } diff --git a/youmubot-db-sql/.sqlx/query-700ec95294d9a4f21e3d7ff53f15f5dc739bffe8fedc19e35cbb576b6dd2e948.json b/youmubot-db-sql/.sqlx/query-df0aa5065268e59c68990ab46ab4a90ec3137398e83b3d0c626209306804399a.json similarity index 80% rename from youmubot-db-sql/.sqlx/query-700ec95294d9a4f21e3d7ff53f15f5dc739bffe8fedc19e35cbb576b6dd2e948.json rename to youmubot-db-sql/.sqlx/query-df0aa5065268e59c68990ab46ab4a90ec3137398e83b3d0c626209306804399a.json index ec5b5f5..bbca50f 100644 --- a/youmubot-db-sql/.sqlx/query-700ec95294d9a4f21e3d7ff53f15f5dc739bffe8fedc19e35cbb576b6dd2e948.json +++ b/youmubot-db-sql/.sqlx/query-df0aa5065268e59c68990ab46ab4a90ec3137398e83b3d0c626209306804399a.json @@ -1,6 +1,6 @@ { "db_name": "SQLite", - "query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\"\n FROM osu_users WHERE user_id = ?", + "query": "SELECT\n user_id as \"user_id: i64\",\n username,\n id as \"id: i64\",\n last_update as \"last_update: DateTime\",\n pp_std, pp_taiko, pp_mania, pp_catch,\n failures as \"failures: u8\",\n std_weighted_map_length\n FROM osu_users WHERE user_id = ?", "describe": { "columns": [ { @@ -47,6 +47,11 @@ "name": "failures: u8", "ordinal": 8, "type_info": "Int64" + }, + { + "name": "std_weighted_map_length", + "ordinal": 9, + "type_info": "Float" } ], "parameters": { @@ -61,8 +66,9 @@ true, true, true, - false + false, + true ] }, - "hash": "700ec95294d9a4f21e3d7ff53f15f5dc739bffe8fedc19e35cbb576b6dd2e948" + "hash": "df0aa5065268e59c68990ab46ab4a90ec3137398e83b3d0c626209306804399a" } diff --git a/youmubot-db-sql/migrations/20240305211858_osu_add_weighted_map_length.sql b/youmubot-db-sql/migrations/20240305211858_osu_add_weighted_map_length.sql new file mode 100644 index 0000000..b20439b --- /dev/null +++ b/youmubot-db-sql/migrations/20240305211858_osu_add_weighted_map_length.sql @@ -0,0 +1,5 @@ +-- Add migration script here + +ALTER TABLE osu_users + ADD COLUMN std_weighted_map_length DOUBLE NULL DEFAULT NULL; + diff --git a/youmubot-db-sql/src/models/osu_user.rs b/youmubot-db-sql/src/models/osu_user.rs index 3de2487..f743e19 100644 --- a/youmubot-db-sql/src/models/osu_user.rs +++ b/youmubot-db-sql/src/models/osu_user.rs @@ -14,6 +14,8 @@ pub struct OsuUser { pub pp_catch: Option, /// Number of consecutive update failures pub failures: u8, + + pub std_weighted_map_length: Option, } impl OsuUser { @@ -30,7 +32,8 @@ impl OsuUser { id as "id: i64", last_update as "last_update: DateTime", pp_std, pp_taiko, pp_mania, pp_catch, - failures as "failures: u8" + failures as "failures: u8", + std_weighted_map_length FROM osu_users WHERE user_id = ?"#, user_id ) @@ -52,7 +55,8 @@ impl OsuUser { id as "id: i64", last_update as "last_update: DateTime", pp_std, pp_taiko, pp_mania, pp_catch, - failures as "failures: u8" + failures as "failures: u8", + std_weighted_map_length FROM osu_users WHERE id = ?"#, osu_id ) @@ -74,7 +78,8 @@ impl OsuUser { id as "id: i64", last_update as "last_update: DateTime", pp_std, pp_taiko, pp_mania, pp_catch, - failures as "failures: u8" + failures as "failures: u8", + std_weighted_map_length FROM osu_users"#, ) .fetch_many(conn) @@ -90,8 +95,8 @@ impl OsuUser { { query!( r#"INSERT - INTO osu_users(user_id, username, id, last_update, pp_std, pp_taiko, pp_mania, pp_catch, failures) - VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?) + INTO osu_users(user_id, username, id, last_update, pp_std, pp_taiko, pp_mania, pp_catch, failures, std_weighted_map_length) + VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT (user_id) WHERE id = ? DO UPDATE SET last_update = excluded.last_update, @@ -100,7 +105,8 @@ impl OsuUser { pp_taiko = excluded.pp_taiko, pp_mania = excluded.pp_mania, pp_catch = excluded.pp_catch, - failures = excluded.failures + failures = excluded.failures, + std_weighted_map_length = excluded.std_weighted_map_length "#, self.user_id, self.username, @@ -111,7 +117,10 @@ impl OsuUser { self.pp_mania, self.pp_catch, self.failures, - self.user_id).execute(conn).await?; + self.std_weighted_map_length, + + self.user_id, + ).execute(conn).await?; Ok(()) } diff --git a/youmubot-osu/src/discord/announcer.rs b/youmubot-osu/src/discord/announcer.rs index e88f2bd..91836c7 100644 --- a/youmubot-osu/src/discord/announcer.rs +++ b/youmubot-osu/src/discord/announcer.rs @@ -1,4 +1,5 @@ use super::db::{OsuSavedUsers, OsuUser}; +use super::OsuClient; use super::{embeds::score_embed, BeatmapWithMode}; use crate::{ discord::beatmap_cache::BeatmapMetaCache, @@ -19,6 +20,7 @@ use serenity::{ }; use std::{convert::TryInto, sync::Arc}; use youmubot_prelude::announcer::CacheAndHttp; +use youmubot_prelude::stream::{FuturesUnordered, TryStreamExt}; use youmubot_prelude::*; /// osu! announcer's unique announcer key. @@ -83,7 +85,12 @@ impl youmubot_prelude::Announcer for Announcer { .unwrap(); osu_user.username = v.into_iter().next().unwrap().username.into(); osu_user.last_update = now; + osu_user.std_weighted_map_length = + Self::std_weighted_map_length(&ctx, &osu_user) + .await + .pls_ok(); let id = osu_user.id; + println!("{:?}", osu_user); ctx.data .read() .await @@ -185,6 +192,31 @@ impl Announcer { .collect(); Ok(scores) } + + async fn std_weighted_map_length(ctx: &Context, u: &OsuUser) -> Result { + let data = ctx.data.read().await; + let client = data.get::().unwrap().clone(); + let cache = data.get::().unwrap(); + let scores = client + .user_best(UserID::ID(u.id), |f| f.mode(Mode::Std).limit(100)) + .await?; + scores + .into_iter() + .enumerate() + .map(|(i, s)| async move { + let beatmap = cache.get_beatmap_default(s.beatmap_id).await?; + const SCALING_FACTOR: f64 = 0.975; + Ok(beatmap + .difficulty + .apply_mods(s.mods, 0.0 /* dont care */) + .drain_length + .as_secs_f64() + * (SCALING_FACTOR.powi(i as i32))) + }) + .collect::>() + .try_fold(0.0, |a, b| future::ready(Ok(a + b))) + .await + } } #[derive(Clone)] diff --git a/youmubot-osu/src/discord/db.rs b/youmubot-osu/src/discord/db.rs index 9f8c2cf..ad54c8d 100644 --- a/youmubot-osu/src/discord/db.rs +++ b/youmubot-osu/src/discord/db.rs @@ -145,6 +145,7 @@ pub struct OsuUser { pub id: u64, pub last_update: DateTime, pub pp: [Option; 4], + pub std_weighted_map_length: Option, /// More than 5 failures => gone pub failures: u8, } @@ -160,6 +161,7 @@ impl From for model::OsuUser { pp_taiko: u.pp[Mode::Taiko as usize], pp_catch: u.pp[Mode::Catch as usize], pp_mania: u.pp[Mode::Mania as usize], + std_weighted_map_length: u.std_weighted_map_length, failures: u.failures, } } @@ -178,6 +180,7 @@ impl From for OsuUser { Mode::Catch => u.pp_catch, Mode::Mania => u.pp_mania, }), + std_weighted_map_length: u.std_weighted_map_length, failures: u.failures, } } diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 27c23fc..90f04ef 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -326,6 +326,7 @@ async fn add_user( failures: 0, last_update: chrono::Utc::now(), pp: [None, None, None, None], + std_weighted_map_length: None, }; data.get::().unwrap().new_user(u).await?; Ok(()) diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index dd639db..9982690 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -36,6 +36,75 @@ impl FromStr for ModeOrTotal { } } +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +enum Align { + Left, + Middle, + Right, +} + +impl Align { + fn pad(self, input: &str, len: usize) -> String { + match self { + Align::Left => format!("{: format!("{:^len$}", input), + Align::Right => format!("{:>len$}", input), + } + } +} + +fn table_formatting + std::fmt::Debug, Ts: AsRef<[[S; N]]>>( + headers: &[&'static str; N], + padding: &[Align; N], + table: Ts, +) -> String { + let table = table.as_ref(); + // get length for each column + let lens = headers + .iter() + .enumerate() + .map(|(i, header)| { + table + .iter() + .map(|r| r.as_ref()[i].as_ref().len()) + .max() + .unwrap_or(0) + .max(header.len()) + }) + .collect::>(); + // paint with message builder + let mut m = MessageBuilder::new(); + m.push_line("```"); + // headers first + for (i, header) in headers.iter().enumerate() { + if i > 0 { + m.push(" | "); + } + m.push(padding[i].pad(header, lens[i])); + } + m.push_line(""); + // separator + m.push_line(format!( + "{:-() + (lens.len() - 1) * 3 + )); + // table itself + for row in table { + let row = row.as_ref(); + for (i, cell) in row.iter().enumerate() { + if i > 0 { + m.push(" | "); + } + let cell = cell.as_ref(); + m.push(padding[i].pad(cell, lens[i])); + } + m.push_line(""); + } + m.push_line("```"); + m.build() +} + #[command("ranks")] #[description = "See the server's ranks"] #[usage = "[mode (Std, Taiko, Catch, Mania) = Std]"] @@ -89,6 +158,7 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR let last_update = last_update.unwrap(); paginate_reply_fn( move |page: u8, ctx: &Context, m: &mut Message| { + use Align::*; const ITEMS_PER_PAGE: usize = 10; let users = users.clone(); Box::pin(async move { @@ -99,51 +169,62 @@ pub async fn server_rank(ctx: &Context, m: &Message, mut args: Args) -> CommandR } let total_len = users.len(); let users = &users[start..end]; - let username_len = users - .iter() - .map(|(_, (_, u))| u.username.len()) - .max() - .unwrap_or(8) - .max(8); - let member_len = users - .iter() - .map(|(_, (mem, _))| mem.len()) - .max() - .unwrap_or(8) - .max(8); - let mut content = MessageBuilder::new(); - content - .push_line("```") + let table = if matches!(mode, ModeOrTotal::Mode(Mode::Std)) { + const HEADERS: [&'static str; 5] = + ["#", "pp", "Map length", "Username", "Member"]; + const ALIGNS: [Align; 5] = [Right, Right, Right, Left, Left]; + + let table = users + .iter() + .enumerate() + .map(|(i, (pp, (mem, ou)))| { + let map_length = match ou.std_weighted_map_length { + Some(len) => { + let trunc_secs = len.floor() as u64; + let minutes = trunc_secs / 60; + let seconds = len - (60 * minutes) as f64; + format!("{}m{:02.2}s", minutes, seconds) + } + None => "unknown".to_owned(), + }; + [ + format!("{}", 1 + i + start), + format!("{:.2}", pp), + map_length, + ou.username.clone().into_owned(), + mem.clone(), + ] + }) + .collect::>(); + table_formatting(&HEADERS, &ALIGNS, table) + } else { + const HEADERS: [&'static str; 4] = ["#", "pp", "Username", "Member"]; + const ALIGNS: [Align; 4] = [Right, Right, Left, Left]; + + let table = users + .iter() + .enumerate() + .map(|(i, (pp, (mem, ou)))| { + [ + format!("{}", 1 + i + start), + format!("{:.2}", pp), + ou.username.clone().into_owned(), + mem.clone(), + ] + }) + .collect::>(); + table_formatting(&HEADERS, &ALIGNS, table) + }; + let content = MessageBuilder::new() + .push_line(table) .push_line(format!( - "Rank | pp | {:uw$} | Member", - "Username", - uw = username_len + "Page **{}**/**{}**. Last updated: {}", + page + 1, + (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE, + last_update.format(""), )) - .push_line(format!( - "------------------{:-4} | {:>8.2} | {:uw$} | {}", - format!("#{}", 1 + id + start), - pp, - u.username, - member, - uw = username_len - )); - } - content.push_line("```").push_line(format!( - "Page **{}**/**{}**. Last updated: {}", - page + 1, - (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE, - last_update.format(""), - )); - m.edit(ctx, EditMessage::new().content(content.to_string())) - .await?; + .build(); + m.edit(ctx, EditMessage::new().content(content)).await?; Ok(true) }) },