From 735b3821027bc27bf57f74a2cac66a212bc72f34 Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Sat, 24 Aug 2024 21:21:01 +0000 Subject: [PATCH] Drop old homegrown Mods for rosu_v2::Mods, implement rate change support (#52) * Move mods to intermode wrapper * Update rust to 1.79 * Move mods from homebrewed impl to rosu * Display mod details * Take clock-rate into account when calculating pp * Allow specifying rate in mods input * Formatting * Fix clippy --- flake.lock | 35 +- flake.nix | 16 +- rust-toolchain | 1 + youmubot-osu/src/discord/announcer.rs | 2 +- youmubot-osu/src/discord/beatmap_cache.rs | 6 +- youmubot-osu/src/discord/display.rs | 14 +- youmubot-osu/src/discord/embeds.rs | 74 +-- youmubot-osu/src/discord/hook.rs | 14 +- youmubot-osu/src/discord/interaction.rs | 11 +- youmubot-osu/src/discord/link_parser.rs | 22 +- youmubot-osu/src/discord/mod.rs | 46 +- youmubot-osu/src/discord/oppai_cache.rs | 20 +- youmubot-osu/src/discord/server_rank.rs | 2 +- youmubot-osu/src/lib.rs | 8 +- youmubot-osu/src/models/mod.rs | 49 +- youmubot-osu/src/models/mods.rs | 530 ++++++++++++++++------ youmubot-osu/src/models/rosu.rs | 353 +++++++------- youmubot-osu/src/request.rs | 25 +- youmubot-prelude/src/announcer.rs | 1 - youmubot-prelude/src/setup.rs | 2 - 20 files changed, 788 insertions(+), 443 deletions(-) create mode 100644 rust-toolchain diff --git a/flake.lock b/flake.lock index 96e7087..5898f6d 100644 --- a/flake.lock +++ b/flake.lock @@ -7,11 +7,11 @@ ] }, "locked": { - "lastModified": 1718730147, - "narHash": "sha256-QmD6B6FYpuoCqu6ZuPJH896ItNquDkn0ulQlOn4ykN8=", + "lastModified": 1724377159, + "narHash": "sha256-ixjje1JO8ucKT41hs6n2NCde1Vc0+Zc2p2gUbJpCsMw=", "owner": "ipetkov", "repo": "crane", - "rev": "32c21c29b034d0a93fdb2379d6fabc40fc3d0e6c", + "rev": "3e47b7a86c19142bd3675da49d6acef488b4dac1", "type": "github" }, "original": { @@ -40,11 +40,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1718530797, - "narHash": "sha256-pup6cYwtgvzDpvpSCFh1TEUjw2zkNpk8iolbKnyFmmU=", + "lastModified": 1724224976, + "narHash": "sha256-Z/ELQhrSd7bMzTO8r7NZgi9g5emh+aRKoCdaAv5fiO0=", "owner": "nixos", "repo": "nixpkgs", - "rev": "b60ebf54c15553b393d144357375ea956f89e9a9", + "rev": "c374d94f1536013ca8e92341b540eba4c22f9c62", "type": "github" }, "original": { @@ -58,7 +58,28 @@ "inputs": { "crane": "crane", "flake-utils": "flake-utils", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" + } + }, + "rust-overlay": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1724466314, + "narHash": "sha256-ltKuK6shQ64uej1mYNtBsDYxttUNFiv9AcHqk0+0NQM=", + "owner": "oxalica", + "repo": "rust-overlay", + "rev": "2b5b3edd96ef336b00622dcabc13788fdef9e3ca", + "type": "github" + }, + "original": { + "owner": "oxalica", + "repo": "rust-overlay", + "type": "github" } }, "systems": { diff --git a/flake.nix b/flake.nix index 28f9142..fcffd7b 100644 --- a/flake.nix +++ b/flake.nix @@ -7,16 +7,24 @@ inputs.nixpkgs.follows = "nixpkgs"; }; flake-utils.url = "github:numtide/flake-utils"; + rust-overlay = { + url = "github:oxalica/rust-overlay"; + inputs.nixpkgs.follows = "nixpkgs"; + }; }; nixConfig = { extra-substituters = [ "https://natsukagami.cachix.org" ]; - trusted-public-keys = [ "natsukagami.cachix.org-1:3U6GV8i8gWEaXRUuXd2S4ASfYgdl2QFPWg4BKPbmYiQ=" ]; + trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" "natsukagami.cachix.org-1:3U6GV8i8gWEaXRUuXd2S4ASfYgdl2QFPWg4BKPbmYiQ=" ]; }; outputs = { self, nixpkgs, flake-utils, ... }@inputs: flake-utils.lib.eachDefaultSystem (system: let - pkgs = import nixpkgs { inherit system; }; - craneLib = inputs.crane.mkLib pkgs; + pkgs = import nixpkgs + { + inherit system; overlays = [ (import inputs.rust-overlay) ]; + }; + craneLib = (inputs.crane.mkLib pkgs).overrideToolchain (p: p.rust-bin.stable."1.79.0".default); + # craneLib = inputs.crane.mkLib pkgs; in rec { packages.youmubot = pkgs.callPackage ./package.nix { inherit craneLib; }; @@ -35,7 +43,7 @@ { inputsFrom = [ packages.youmubot ]; - buildInputs = with pkgs; [ rustc rustfmt clippy sqlx-cli rust-analyzer ]; + buildInputs = with pkgs; [ rustfmt clippy sqlx-cli rust-analyzer ]; nativeBuildInputs = nixpkgs.lib.optionals pkgs.stdenv.isLinux (with pkgs; [ pkg-config diff --git a/rust-toolchain b/rust-toolchain new file mode 100644 index 0000000..b3a8c61 --- /dev/null +++ b/rust-toolchain @@ -0,0 +1 @@ +1.79.0 diff --git a/youmubot-osu/src/discord/announcer.rs b/youmubot-osu/src/discord/announcer.rs index 261d82e..a725daa 100644 --- a/youmubot-osu/src/discord/announcer.rs +++ b/youmubot-osu/src/discord/announcer.rs @@ -27,7 +27,7 @@ use crate::{ discord::oppai_cache::BeatmapContent, models::{Mode, Score, User, UserEventRank}, request::UserID, - Client as Osu, + OsuClient as Osu, }; use super::db::OsuUser; diff --git a/youmubot-osu/src/discord/beatmap_cache.rs b/youmubot-osu/src/discord/beatmap_cache.rs index 0a89c1f..3fb17f4 100644 --- a/youmubot-osu/src/discord/beatmap_cache.rs +++ b/youmubot-osu/src/discord/beatmap_cache.rs @@ -5,14 +5,14 @@ use youmubot_prelude::*; use crate::{ models::{ApprovalStatus, Beatmap, Mode}, - Client, + OsuClient, }; /// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling. /// Does not cache non-Ranked beatmaps. #[derive(Clone)] pub struct BeatmapMetaCache { - client: Arc, + client: Arc, pool: Pool, } @@ -28,7 +28,7 @@ impl TypeMapKey for BeatmapMetaCache { impl BeatmapMetaCache { /// Create a new beatmap cache. - pub fn new(client: Arc, pool: Pool) -> Self { + pub fn new(client: Arc, pool: Pool) -> Self { BeatmapMetaCache { client, pool } } diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index 9d78b95..1304e39 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -211,7 +211,7 @@ mod scores { let beatmap = meta_cache.get_beatmap(play.beatmap_id, mode).await?; let info = { let b = oppai.get_beatmap(beatmap.beatmap_id).await?; - b.get_info_with(mode, play.mods).ok() + b.get_info_with(mode, &play.mods).ok() }; Ok((beatmap, info)) as Result<(Beatmap, Option)> }) @@ -236,7 +236,7 @@ mod scores { p.count_50, p.count_miss, ), - p.mods, + &p.mods, ) .ok() .map(|pp| format!("{:.2}[?]", pp)) @@ -291,7 +291,7 @@ mod scores { beatmap.artist, beatmap.title, beatmap.difficulty_name, - beatmap.short_link(Some(self.mode), Some(play.mods)), + beatmap.short_link(Some(self.mode), &play.mods), ) }) .unwrap_or_else(|| "FETCH_FAILED".to_owned()) @@ -359,13 +359,11 @@ mod beatmapset { ctx: &Context, beatmapset: Vec, mode: Option, - mods: Option, + mods: Mods, reply_to: &Message, guild_id: Option, message: impl AsRef, ) -> Result { - let mods = mods.unwrap_or(Mods::NOMOD); - if beatmapset.is_empty() { return Ok(false); } @@ -405,7 +403,7 @@ mod beatmapset { env.oppai .get_beatmap(b.beatmap_id) .await - .and_then(move |v| v.get_possible_pp_with(self.mode.unwrap_or(b.mode), self.mods)) + .and_then(move |v| v.get_possible_pp_with(self.mode.unwrap_or(b.mode), &self.mods)) } } @@ -446,7 +444,7 @@ mod beatmapset { crate::discord::embeds::beatmap_embed( map, self.mode.unwrap_or(map.mode), - self.mods, + &self.mods, info, ) .footer({ diff --git a/youmubot-osu/src/discord/embeds.rs b/youmubot-osu/src/discord/embeds.rs index 4c66846..c760d78 100644 --- a/youmubot-osu/src/discord/embeds.rs +++ b/youmubot-osu/src/discord/embeds.rs @@ -25,7 +25,7 @@ pub(crate) fn grouped_number(num: u64) -> String { b.build() } -fn beatmap_description(b: &Beatmap) -> String { +fn beatmap_description(b: &Beatmap, mods: &Mods) -> String { MessageBuilder::new() .push_bold_line(b.approval.to_string()) .push({ @@ -59,13 +59,24 @@ fn beatmap_description(b: &Beatmap) -> String { .collect::>() .join(" "), ) + .push_line(mod_details(mods).unwrap_or("".into())) .build() } +fn mod_details(mods: &Mods) -> Option> { + let mut d = mods.details(); + if d.is_empty() { + None + } else { + d.insert(0, "**Mods**:".to_owned()); + Some(Cow::from(d.join("\n- "))) + } +} + pub fn beatmap_offline_embed( b: &'_ crate::discord::oppai_cache::BeatmapContent, m: Mode, - mods: Mods, + mods: &Mods, ) -> Result<(CreateEmbed, Vec)> { let bm = b.content.clone(); let metadata = b.metadata.clone(); @@ -150,7 +161,7 @@ fn beatmap_title( artist: impl AsRef, title: impl AsRef, difficulty: impl AsRef, - mods: Mods, + mods: &Mods, ) -> String { let mod_str = if mods == Mods::NOMOD { "".to_owned() @@ -168,7 +179,7 @@ fn beatmap_title( .build() } -pub fn beatmap_embed(b: &'_ Beatmap, m: Mode, mods: Mods, info: BeatmapInfoWithPP) -> CreateEmbed { +pub fn beatmap_embed(b: &'_ Beatmap, m: Mode, mods: &Mods, info: BeatmapInfoWithPP) -> CreateEmbed { let diff = b.difficulty.apply_mods(mods, info.0.stars); CreateEmbed::new() .title(beatmap_title(&b.artist, &b.title, &b.difficulty_name, mods)) @@ -192,7 +203,7 @@ pub fn beatmap_embed(b: &'_ Beatmap, m: Mode, mods: Mods, info: BeatmapInfoWithP )) }) .field("Information", diff.format_info(m, mods, b), false) - .description(beatmap_description(b)) + .description(beatmap_description(b, mods)) } const MAX_DIFFS: usize = 25 - 4; @@ -222,7 +233,7 @@ pub fn beatmapset_embed(bs: &'_ [Beatmap], m: Option) -> CreateEmbed { b.beatmapset_id )) .color(0xffb6c1) - .description(beatmap_description(b)) + .description(beatmap_description(b, Mods::NOMOD)) .fields(bs.iter().rev().take(MAX_DIFFS).rev().map(|b: &Beatmap| { ( format!("[{}]", b.difficulty_name), @@ -292,7 +303,7 @@ impl<'a> ScoreEmbedBuilder<'a> { let content = self.content; let u = &self.u; let accuracy = s.accuracy(mode); - let info = content.get_info_with(mode, s.mods).ok(); + let info = content.get_info_with(mode, &s.mods).ok(); let stars = info .as_ref() .map(|info| info.stars) @@ -322,7 +333,7 @@ impl<'a> ScoreEmbedBuilder<'a> { mode, Some(s.max_combo as usize), Accuracy::ByCount(s.count_300, s.count_100, s.count_50, s.count_miss), - s.mods, + &s.mods, ) .ok() .map(|pp| (pp, format!("{:.2}pp [?]", pp))) @@ -333,7 +344,7 @@ impl<'a> ScoreEmbedBuilder<'a> { mode, None, Accuracy::ByCount(s.count_300 + s.count_miss, s.count_100, s.count_50, 0), - s.mods, + &s.mods, ) .ok() .filter(|&v| pp.as_ref().map(|&(origin, _)| origin < v).unwrap_or(false)) @@ -377,12 +388,34 @@ impl<'a> ScoreEmbedBuilder<'a> { .or(s.global_rank) .map(|v| format!(" | #{} on Global Rankings!", v)) .unwrap_or_else(|| "".to_owned()); - let diff = b.difficulty.apply_mods(s.mods, stars); + let diff = b.difficulty.apply_mods(&s.mods, stars); let creator = if b.difficulty_name.contains("'s") { "".to_owned() } else { format!("by {} ", b.creator) }; + let mod_details = mod_details(&s.mods); + let description_fields = [ + Some( + format!( + "**Played**: {} {} {}", + s.date.format(""), + s.link() + .map(|s| format!("[[Score]]({})", s).into()) + .unwrap_or(Cow::from("")), + s.replay_download_link() + .map(|s| format!("[[Replay]]({})", s).into()) + .unwrap_or(Cow::from("")), + ) + .into(), + ), + pp_gained.as_ref().map(|v| (&v[..]).into()), + mod_details, + ] + .into_iter() + .flatten() + .collect::>() + .join("\n"); let mut m = CreateEmbed::new() .author( CreateEmbedAuthor::new(&u.username) @@ -411,18 +444,7 @@ impl<'a> ScoreEmbedBuilder<'a> { .push(world_record) .build(), ) - .description(format!( - r#"**Played**: {} {} {} -{}"#, - s.date.format(""), - s.link() - .map(|s| format!("[[Score]]({})", s).into()) - .unwrap_or(Cow::from("")), - s.replay_download_link() - .map(|s| format!("[[Replay]]({})", s).into()) - .unwrap_or(Cow::from("")), - pp_gained.as_ref().map(|v| &v[..]).unwrap_or(""), - )) + .description(description_fields) .thumbnail(b.thumbnail_url()) .field( "Score stats", @@ -442,9 +464,9 @@ impl<'a> ScoreEmbedBuilder<'a> { ), true, ) - .field("Map stats", diff.format_info(mode, s.mods, b), false); + .field("Map stats", diff.format_info(mode, &s.mods, b), false); let mut footer = self.footer.take().unwrap_or_default(); - if mode != Mode::Std && s.mods != Mods::NOMOD { + if mode != Mode::Std && &s.mods != Mods::NOMOD { footer += " Star difficulty does not reflect game mods."; } if !footer.is_empty() { @@ -557,8 +579,8 @@ pub(crate) fn user_embed( .push(format!( "> {}", map.difficulty - .apply_mods(v.mods, info.stars) - .format_info(mode, v.mods, &map) + .apply_mods(&v.mods, info.stars) + .format_info(mode, &v.mods, &map) .replace('\n', "\n> ") )) .build(), diff --git a/youmubot-osu/src/discord/hook.rs b/youmubot-osu/src/discord/hook.rs index 2efe81f..6ff2797 100644 --- a/youmubot-osu/src/discord/hook.rs +++ b/youmubot-osu/src/discord/hook.rs @@ -123,10 +123,11 @@ pub fn dot_osu_hook<'a>( let env = ctx.data.read().await.get::().unwrap().clone(); let (beatmap, _) = env.oppai.download_beatmap_from_url(&url).await.ok()?; + let m = Mode::from(beatmap.content.mode as u8); crate::discord::embeds::beatmap_offline_embed( &beatmap, - Mode::from(beatmap.content.mode as u8), /*For now*/ - msg.content.trim().parse().unwrap_or(Mods::NOMOD), + m, /*For now*/ + &Mods::from_str(msg.content.trim(), m).unwrap_or_default(), ) .pls_ok() } @@ -152,10 +153,11 @@ pub fn dot_osu_hook<'a>( beatmaps .into_iter() .filter_map(|beatmap| { + let m = Mode::from(beatmap.content.mode as u8); crate::discord::embeds::beatmap_offline_embed( &beatmap, - Mode::from(beatmap.content.mode as u8), /*For now*/ - msg.content.trim().parse().unwrap_or(Mods::NOMOD), + m, /*For now*/ + &Mods::from_str(msg.content.trim(), m).unwrap_or_default(), ) .pls_ok() }) @@ -300,7 +302,7 @@ async fn handle_beatmap<'a, 'b>( .embed(beatmap_embed( beatmap, mode.unwrap_or(beatmap.mode), - mods, + &mods, info, )) .components(vec![beatmap_components(reply_to.guild_id)]) @@ -321,7 +323,7 @@ async fn handle_beatmapset<'a, 'b>( ctx, beatmaps, mode, - None, + Mods::default(), reply_to, reply_to.guild_id, format!("Beatmapset information for `{}`", link), diff --git a/youmubot-osu/src/discord/interaction.rs b/youmubot-osu/src/discord/interaction.rs index 79c69e2..67c8f13 100644 --- a/youmubot-osu/src/discord/interaction.rs +++ b/youmubot-osu/src/discord/interaction.rs @@ -147,18 +147,21 @@ pub fn handle_last_button<'a>( .unwrap(); let BeatmapWithMode(b, m) = &bm; - let mods = mods_def.unwrap_or(Mods::NOMOD); + let mods = mods_def.unwrap_or_default(); let info = env .oppai .get_beatmap(b.beatmap_id) .await? - .get_possible_pp_with(*m, mods)?; + .get_possible_pp_with(*m, &mods)?; comp.create_response( &ctx, serenity::all::CreateInteractionResponse::Message( CreateInteractionResponseMessage::new() - .content(format!("Information for beatmap `{}`", bm.short_link(mods))) - .embed(beatmap_embed(b, *m, mods, info)) + .content(format!( + "Information for beatmap `{}`", + bm.short_link(&mods) + )) + .embed(beatmap_embed(b, *m, &mods, info)) .components(vec![beatmap_components(comp.guild_id)]), ), ) diff --git a/youmubot-osu/src/discord/link_parser.rs b/youmubot-osu/src/discord/link_parser.rs index 6f9df08..7010aae 100644 --- a/youmubot-osu/src/discord/link_parser.rs +++ b/youmubot-osu/src/discord/link_parser.rs @@ -2,6 +2,7 @@ use std::str::FromStr; use crate::models::*; use lazy_static::lazy_static; +use mods::UnparsedMods; use regex::Regex; use stream::Stream; use youmubot_prelude::*; @@ -22,7 +23,7 @@ pub struct ToPrint<'a> { lazy_static! { // Beatmap(set) hooks static ref OLD_LINK_REGEX: Regex = Regex::new( - r"(?:https?://)?osu\.ppy\.sh/(?Ps|b)/(?P\d+)(?:[\&\?]m=(?P[0123]))?(?:\+(?P[A-Z]+))?" + r"(?:https?://)?osu\.ppy\.sh/(?Ps|b|beatmaps)/(?P\d+)(?:[\&\?]m=(?P[0123]))?(?:\+(?P[A-Z]+))?" ).unwrap(); static ref NEW_LINK_REGEX: Regex = Regex::new( r"(?:https?://)?osu\.ppy\.sh/beatmapsets/(?P\d+)/?(?:\#(?Posu|taiko|fruits|mania)(?:/(?P\d+)|/?))?(?:\+(?P[A-Z]+))?" @@ -51,12 +52,12 @@ pub fn parse_old_links<'a>( .transpose()? .map(Mode::from); let embed = match req_type { - "b" => { + "b" | "beatmaps" => { // collect beatmap info let mods = capture .name("mods") - .and_then(|v| Mods::from_str(v.as_str()).pls_ok()) - .unwrap_or(Mods::NOMOD); + .and_then(|v| UnparsedMods::from_str(v.as_str()).pls_ok()) + .unwrap_or_default(); EmbedType::from_beatmap_id(env, capture["id"].parse()?, mode, mods).await } "s" => EmbedType::from_beatmapset_id(env, capture["id"].parse()?).await, @@ -90,8 +91,8 @@ pub fn parse_new_links<'a>( Some(beatmap_id) => { let mods = capture .name("mods") - .and_then(|v| Mods::from_str(v.as_str()).pls_ok()) - .unwrap_or(Mods::NOMOD); + .and_then(|v| UnparsedMods::from_str(v.as_str()).pls_ok()) + .unwrap_or_default(); EmbedType::from_beatmap_id(env, beatmap_id, mode, mods).await } None => { @@ -122,8 +123,8 @@ pub fn parse_short_links<'a>( let id: u64 = capture.name("id").unwrap().as_str().parse()?; let mods = capture .name("mods") - .and_then(|v| Mods::from_str(v.as_str()).pls_ok()) - .unwrap_or(Mods::NOMOD); + .and_then(|v| UnparsedMods::from_str(v.as_str()).pls_ok()) + .unwrap_or_default(); let embed = EmbedType::from_beatmap_id(env, id, mode, mods).await?; Ok(ToPrint { embed, link, mode }) }) @@ -136,18 +137,19 @@ impl EmbedType { env: &OsuEnv, beatmap_id: u64, mode: Option, - mods: Mods, + mods: UnparsedMods, ) -> Result { let bm = match mode { Some(mode) => env.beatmaps.get_beatmap(beatmap_id, mode).await?, None => env.beatmaps.get_beatmap_default(beatmap_id).await?, }; + let mods = mods.to_mods(mode.unwrap_or(bm.mode))?; let info = { let mode = mode.unwrap_or(bm.mode); env.oppai .get_beatmap(bm.beatmap_id) .await - .and_then(|b| b.get_possible_pp_with(mode, mods))? + .and_then(|b| b.get_possible_pp_with(mode, &mods))? }; Ok(Self::Beatmap(Box::new(bm), info, mods)) } diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 8c2a624..474d7bb 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -25,12 +25,15 @@ use youmubot_prelude::announcer::AnnouncerHandler; use youmubot_prelude::*; use crate::{ - discord::beatmap_cache::BeatmapMetaCache, - discord::display::ScoreListStyle, - discord::oppai_cache::{BeatmapCache, BeatmapInfo}, + discord::{ + beatmap_cache::BeatmapMetaCache, + display::ScoreListStyle, + oppai_cache::{BeatmapCache, BeatmapInfo}, + }, models::{Beatmap, Mode, Mods, Score, User}, + mods::UnparsedMods, request::{BeatmapRequestKind, UserID}, - Client as OsuHttpClient, + OsuClient as OsuHttpClient, }; mod announcer; @@ -49,7 +52,7 @@ mod server_rank; pub(crate) struct OsuClient; impl TypeMapKey for OsuClient { - type Value = Arc; + type Value = Arc; } /// The environment for osu! app commands. @@ -60,7 +63,7 @@ pub struct OsuEnv { pub(crate) saved_users: OsuSavedUsers, pub(crate) last_beatmaps: OsuLastBeatmap, // clients - pub(crate) client: Arc, + pub(crate) client: Arc, pub(crate) oppai: BeatmapCache, pub(crate) beatmaps: BeatmapMetaCache, } @@ -199,8 +202,8 @@ pub async fn mania(ctx: &Context, msg: &Message, args: Args) -> CommandResult { pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode); impl BeatmapWithMode { - pub fn short_link(&self, mods: Mods) -> String { - self.0.short_link(Some(self.1), Some(mods)) + pub fn short_link(&self, mods: &Mods) -> String { + self.0.short_link(Some(self.1), mods) } fn mode(&self) -> Mode { @@ -623,14 +626,17 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult match b { Some((bm, mods_def)) => { - let mods = args.find::().ok().or(mods_def).unwrap_or(Mods::NOMOD); + let mods = match args.find::().ok() { + Some(m) => m.to_mods(bm.mode())?, + None => mods_def.unwrap_or_default(), + }; if beatmapset { let beatmapset = env.beatmaps.get_beatmapset(bm.0.beatmapset_id).await?; display::display_beatmapset( ctx, beatmapset, None, - Some(mods), + mods, msg, msg.guild_id, "Here is the beatmapset you requested!", @@ -642,13 +648,13 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult .oppai .get_beatmap(bm.0.beatmap_id) .await? - .get_possible_pp_with(bm.1, mods)?; + .get_possible_pp_with(bm.1, &mods)?; msg.channel_id .send_message( &ctx, CreateMessage::new() .content("Here is the beatmap you requested!") - .embed(beatmap_embed(&bm.0, bm.1, mods, info)) + .embed(beatmap_embed(&bm.0, bm.1, &mods, info)) .components(vec![beatmap_components(msg.guild_id)]) .reference_message(msg), ) @@ -683,14 +689,18 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul } }; let mode = bm.1; - let mods = args.find::().ok().unwrap_or_default(); + let mods = args + .find::() + .ok() + .unwrap_or_default() + .to_mods(mode)?; let style = args .single::() .unwrap_or(ScoreListStyle::Grid); let username_arg = args.single::().ok(); let user = to_user_id_query(username_arg, &env, msg.author.id).await?; - let scores = do_check(&env, &bm, mods, &user).await?; + let scores = do_check(&env, &bm, &mods, &user).await?; if scores.is_empty() { msg.reply(&ctx, "No scores found").await?; @@ -702,7 +712,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul format!( "Here are the scores by `{}` on `{}`!", &user, - bm.short_link(mods) + bm.short_link(&mods) ), ) .await?; @@ -716,7 +726,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul pub(crate) async fn do_check( env: &OsuEnv, bm: &BeatmapWithMode, - mods: Mods, + mods: &Mods, user: &UserID, ) -> Result> { let BeatmapWithMode(b, m) = bm; @@ -856,7 +866,7 @@ async fn get_user( .oppai .get_beatmap(m.beatmap_id) .await? - .get_info_with(mode, m.mods)?; + .get_info_with(mode, &m.mods)?; Some((m, BeatmapWithMode(beatmap, mode), info)) } None => None, @@ -907,7 +917,7 @@ pub(in crate::discord) async fn calculate_weighted_map_length( let beatmap = cache.get_beatmap(s.beatmap_id, mode).await?; Ok(beatmap .difficulty - .apply_mods(s.mods, 0.0 /* dont care */) + .apply_mods(&s.mods, 0.0 /* dont care */) .drain_length .as_secs_f64()) as Result<_> }) diff --git a/youmubot-osu/src/discord/oppai_cache.rs b/youmubot-osu/src/discord/oppai_cache.rs index de3388b..d93b518 100644 --- a/youmubot-osu/src/discord/oppai_cache.rs +++ b/youmubot-osu/src/discord/oppai_cache.rs @@ -74,15 +74,20 @@ impl BeatmapContent { mode: Mode, combo: Option, accuracy: Accuracy, - mods: Mods, + mods: &Mods, ) -> Result { + let clock = match mods.inner.clock_rate() { + None => bail!("cannot calculate pp for mods: {}", mods), + Some(clock) => clock as f64, + }; let mut perf = self .content .performance() .mode_or_ignore(mode.into()) .accuracy(accuracy.into()) .misses(accuracy.misses() as u32) - .mods(mods.bits() as u32); + .mods(mods.bits()) + .clock_rate(clock); if let Some(combo) = combo { perf = perf.combo(combo as u32); } @@ -91,12 +96,17 @@ impl BeatmapContent { } /// Get info given mods. - pub fn get_info_with(&self, mode: Mode, mods: Mods) -> Result { + pub fn get_info_with(&self, mode: Mode, mods: &Mods) -> Result { + let clock = match mods.inner.clock_rate() { + None => bail!("cannot calculate info for mods: {}", mods), + Some(clock) => clock as f64, + }; let attrs = self .content .performance() .mode_or_ignore(mode.into()) - .mods(mods.bits() as u32) + .mods(mods.bits()) + .clock_rate(clock) .calculate(); Ok(BeatmapInfo { objects: self.content.hit_objects.len(), @@ -105,7 +115,7 @@ impl BeatmapContent { }) } - pub fn get_possible_pp_with(&self, mode: Mode, mods: Mods) -> Result { + pub fn get_possible_pp_with(&self, mode: Mode, mods: &Mods) -> Result { let pp: [f64; 4] = [ self.get_pp_from(mode, None, Accuracy::ByValue(95.0, 0), mods)?, self.get_pp_from(mode, None, Accuracy::ByValue(98.0, 0), mods)?, diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index 75fca05..3b07321 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -446,7 +446,7 @@ pub async fn get_leaderboard( score.count_50, score.count_miss, ), - score.mods, + &score.mods, ) .ok() .map(|v| (false, v)) diff --git a/youmubot-osu/src/lib.rs b/youmubot-osu/src/lib.rs index c37cbed..e540261 100644 --- a/youmubot-osu/src/lib.rs +++ b/youmubot-osu/src/lib.rs @@ -14,7 +14,7 @@ pub mod request; /// Client is the client that will perform calls to the osu! api server. #[derive(Clone)] -pub struct Client { +pub struct OsuClient { rosu: Arc, user_header_cache: Arc>>>, @@ -30,15 +30,15 @@ pub fn vec_try_into>(v: Vec) -> Result, Ok(res) } -impl Client { +impl OsuClient { /// Create a new client from the given API key. - pub async fn new(client_id: u64, client_secret: impl Into) -> Result { + pub async fn new(client_id: u64, client_secret: impl Into) -> Result { let rosu = rosu_v2::OsuBuilder::new() .client_id(client_id) .client_secret(client_secret) .build() .await?; - Ok(Client { + Ok(OsuClient { rosu: Arc::new(rosu), user_header_cache: Arc::new(Mutex::new(HashMap::new())), }) diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index d790ce5..52a62eb 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -1,4 +1,5 @@ use chrono::{DateTime, Utc}; +use rosu_v2::prelude::GameModIntermode; use serde::{Deserialize, Serialize}; use std::fmt; use std::time::Duration; @@ -85,39 +86,44 @@ impl Difficulty { // then convert back self.od = (79.0 - (hit_timing - 0.5)) / 6.0; } - fn apply_length_by_ratio(&mut self, mul: u32, div: u32) { - self.bpm = self.bpm / (mul as f64) * (div as f64); // Inverse since bpm increases while time decreases - self.drain_length = self.drain_length * mul / div; - self.total_length = self.total_length * mul / div; + fn apply_length_by_ratio(&mut self, ratio: f64) { + self.bpm /= ratio; // Inverse since bpm increases while time decreases + self.drain_length = Duration::from_secs_f64(self.drain_length.as_secs_f64() * ratio); + self.total_length = Duration::from_secs_f64(self.total_length.as_secs_f64() * ratio); } /// Apply mods to the given difficulty. /// Note that `stars`, `aim` and `speed` cannot be calculated from this alone. - pub fn apply_mods(&self, mods: Mods, updated_stars: f64) -> Difficulty { + pub fn apply_mods(&self, mods: &Mods, updated_stars: f64) -> Difficulty { let mut diff = Difficulty { stars: updated_stars, ..self.clone() }; // Apply mods one by one - if mods.contains(Mods::EZ) { + if mods.inner.contains_intermode(GameModIntermode::Easy) { diff.apply_everything_by_ratio(0.5); } - if mods.contains(Mods::HR) { + if mods.inner.contains_intermode(GameModIntermode::HardRock) { let old_cs = diff.cs; diff.apply_everything_by_ratio(1.4); // CS is changed by 1.3 tho diff.cs = old_cs * 1.3; } - if mods.contains(Mods::HT) { - diff.apply_ar_by_time_ratio(4.0 / 3.0); - diff.apply_od_by_time_ratio(4.0 / 3.0); - diff.apply_length_by_ratio(4, 3); - } - if mods.contains(Mods::DT) { - diff.apply_ar_by_time_ratio(2.0 / 3.0); - diff.apply_od_by_time_ratio(2.0 / 3.0); - diff.apply_length_by_ratio(2, 3); + if let Some(ratio) = mods.inner.clock_rate() { + diff.apply_length_by_ratio(1.0 / ratio as f64); + diff.apply_ar_by_time_ratio(1.0 / ratio as f64); + diff.apply_od_by_time_ratio(1.0 / ratio as f64); } + // if mods.contains(Mods::HT) { + // diff.apply_ar_by_time_ratio(4.0 / 3.0); + // diff.apply_od_by_time_ratio(4.0 / 3.0); + // diff.apply_length_by_ratio(4, 3); + // } + // if mods.contains(Mods::DT) { + // diff.apply_ar_by_time_ratio(2.0 / 3.0); + // diff.apply_od_by_time_ratio(2.0 / 3.0); + // diff.apply_length_by_ratio(2, 3); + // } diff } @@ -126,7 +132,7 @@ impl Difficulty { pub fn format_info<'a>( &self, mode: Mode, - mods: Mods, + mods: &Mods, original_beatmap: impl Into> + 'a, ) -> String { let original_beatmap = original_beatmap.into(); @@ -146,7 +152,7 @@ impl Difficulty { original_beatmap.download_link(BeatmapSite::Bancho), original_beatmap.download_link(BeatmapSite::Beatconnect), original_beatmap.download_link(BeatmapSite::Chimu), - original_beatmap.short_link(Some(mode), Some(mods)) + original_beatmap.short_link(Some(mode), mods) ) }) .unwrap_or("**Uploaded**".to_owned()), @@ -405,7 +411,7 @@ impl Beatmap { } /// Return a parsable short link. - pub fn short_link(&self, override_mode: Option, mods: Option) -> String { + pub fn short_link(&self, override_mode: Option, mods: &Mods) -> String { format!( "/b/{}{}{}", self.beatmap_id, @@ -413,8 +419,7 @@ impl Beatmap { Some(mode) if mode != self.mode => format!("/{}", mode.as_str_new_site()), _ => "".to_owned(), }, - mods.map(|m| format!("{}", m)) - .unwrap_or_else(|| "".to_owned()), + mods.strip_lazer(override_mode.unwrap_or(Mode::Std)) ) } @@ -587,7 +592,7 @@ impl fmt::Display for Rank { } } -#[derive(Clone, Debug, Serialize, Deserialize)] +#[derive(Clone, Debug)] pub struct Score { pub id: Option, // No id if you fail pub user_id: u64, diff --git a/youmubot-osu/src/models/mods.rs b/youmubot-osu/src/models/mods.rs index 685d49c..58efa91 100644 --- a/youmubot-osu/src/models/mods.rs +++ b/youmubot-osu/src/models/mods.rs @@ -1,169 +1,439 @@ -use serde::{Deserialize, Serialize}; +use regex::Regex; +use rosu::{GameModIntermode, GameMods}; +use rosu_v2::model::mods as rosu; +use rosu_v2::prelude::GameModsIntermode; +use std::borrow::Cow; use std::fmt; +use std::str::FromStr; +use youmubot_prelude::*; + +use crate::Mode; const LAZER_TEXT: &str = "v2"; -bitflags::bitflags! { - /// The mods available to osu! - #[derive(std::default::Default, Serialize, Deserialize)] - pub struct Mods: u64 { - const NF = 1 << 0; - const EZ = 1 << 1; - const TD = 1 << 2; - const HD = 1 << 3; - const HR = 1 << 4; - const SD = 1 << 5; - const DT = 1 << 6; - const RX = 1 << 7; - const HT = 1 << 8; - const NC = 1 << 9; - const FL = 1 << 10; - const AT = 1 << 11; - const SO = 1 << 12; - const AP = 1 << 13; - const PF = 1 << 14; - const KEY4 = 1 << 15; /* TODO: what are these abbreviated to? */ - const KEY5 = 1 << 16; - const KEY6 = 1 << 17; - const KEY7 = 1 << 18; - const KEY8 = 1 << 19; - const FADEIN = 1 << 20; - const RANDOM = 1 << 21; - const CINEMA = 1 << 22; - const TARGET = 1 << 23; - const KEY9 = 1 << 24; - const KEYCOOP = 1 << 25; - const KEY1 = 1 << 26; - const KEY3 = 1 << 27; - const KEY2 = 1 << 28; - const SCOREV2 = 1 << 29; +lazy_static::lazy_static! { + // Beatmap(set) hooks + static ref MODS: Regex = Regex::new( + // r"(?:https?://)?osu\.ppy\.sh/(?Ps|b|beatmaps)/(?P\d+)(?:[\&\?]m=(?P[0123]))?(?:\+(?P[A-Z]+))?" + r"^((\+?)(?P([A-Za-z0-9][A-Za-z])+))?(@(?P\d(\.\d+)?)x)?(v2)?$" + ).unwrap(); +} - // Made up flags - const LAZER = 1 << 59; - const UNKNOWN = 1 << 60; +#[derive(Debug, Clone, PartialEq)] +pub struct UnparsedMods { + mods: Cow<'static, str>, + clock: Option, +} + +impl Default for UnparsedMods { + fn default() -> Self { + Self { + mods: "".into(), + clock: None, + } } } -impl Mods { - pub const NOMOD: Mods = Mods::empty(); - pub const TOUCH_DEVICE: Mods = Self::TD; - pub const NOVIDEO: Mods = Self::TD; /* never forget */ - pub const SPEED_CHANGING: Mods = - Mods::from_bits_truncate(Self::DT.bits | Self::HT.bits | Self::NC.bits); - pub const MAP_CHANGING: Mods = - Mods::from_bits_truncate(Self::HR.bits | Self::EZ.bits | Self::SPEED_CHANGING.bits); +impl FromStr for UnparsedMods { + type Err = String; + + fn from_str(s: &str) -> std::result::Result { + if s.is_empty() { + return Ok(UnparsedMods::default()); + } + let ms = match MODS.captures(s) { + Some(m) => m, + None => return Err(format!("invalid mods: {}", s)), + }; + let mods = ms.name("mods").map(|v| v.as_str().to_owned()); + if let Some(mods) = &mods { + if GameModsIntermode::try_from_acronyms(mods).is_none() { + return Err(format!("invalid mod sequence: {}", mods)); + } + } + Ok(Self { + mods: mods.map(|v| v.into()).unwrap_or("".into()), + clock: ms + .name("clock") + .map(|v| v.as_str().parse::().unwrap()) + .filter(|v| *v > 0.0), + }) + } } -const MODS_WITH_NAMES: &[(Mods, &str)] = &[ - (Mods::NF, "NF"), - (Mods::EZ, "EZ"), - (Mods::TD, "TD"), - (Mods::HD, "HD"), - (Mods::HR, "HR"), - (Mods::SD, "SD"), - (Mods::DT, "DT"), - (Mods::RX, "RX"), - (Mods::HT, "HT"), - (Mods::NC, "NC"), - (Mods::FL, "FL"), - (Mods::AT, "AT"), - (Mods::SO, "SO"), - (Mods::AP, "AP"), - (Mods::PF, "PF"), - (Mods::KEY1, "1K"), - (Mods::KEY2, "2K"), - (Mods::KEY3, "3K"), - (Mods::KEY4, "4K"), - (Mods::KEY5, "5K"), - (Mods::KEY6, "6K"), - (Mods::KEY7, "7K"), - (Mods::KEY8, "8K"), - (Mods::KEY9, "9K"), - (Mods::UNKNOWN, "??"), -]; +impl UnparsedMods { + /// Convert to [Mods]. + pub fn to_mods(&self, mode: Mode) -> Result { + use rosu_v2::prelude::*; + let mut mods = Mods::from_str(&self.mods, mode)?; + if let Some(clock) = self.clock { + let has_night_day_core = mods.inner.contains_intermode(GameModIntermode::Nightcore) + || mods.inner.contains_intermode(GameModIntermode::Daycore); + mods.inner.remove_all_intermode([ + GameModIntermode::Daycore, + GameModIntermode::Nightcore, + GameModIntermode::DoubleTime, + GameModIntermode::HalfTime, + ]); + let mut speed_change = Some(clock); + let adjust_pitch: Option = None; + if clock < 1.0 { + speed_change = speed_change.filter(|v| *v != 0.75); + mods.inner.insert(if has_night_day_core { + match mode { + Mode::Std => GameMod::DaycoreOsu(DaycoreOsu { speed_change }), + Mode::Taiko => GameMod::DaycoreTaiko(DaycoreTaiko { speed_change }), + Mode::Catch => GameMod::DaycoreCatch(DaycoreCatch { speed_change }), + Mode::Mania => GameMod::DaycoreMania(DaycoreMania { speed_change }), + } + } else { + match mode { + Mode::Std => GameMod::HalfTimeOsu(HalfTimeOsu { + speed_change, + adjust_pitch, + }), + Mode::Taiko => GameMod::HalfTimeTaiko(HalfTimeTaiko { + speed_change, + adjust_pitch, + }), + Mode::Catch => GameMod::HalfTimeCatch(HalfTimeCatch { + speed_change, + adjust_pitch, + }), + Mode::Mania => GameMod::HalfTimeMania(HalfTimeMania { + speed_change, + adjust_pitch, + }), + } + }) + } + if clock > 1.0 { + speed_change = speed_change.filter(|v| *v != 1.5); + mods.inner.insert(if has_night_day_core { + match mode { + Mode::Std => GameMod::NightcoreOsu(NightcoreOsu { speed_change }), + Mode::Taiko => GameMod::NightcoreTaiko(NightcoreTaiko { speed_change }), + Mode::Catch => GameMod::NightcoreCatch(NightcoreCatch { speed_change }), + Mode::Mania => GameMod::NightcoreMania(NightcoreMania { speed_change }), + } + } else { + match mode { + Mode::Std => GameMod::DoubleTimeOsu(DoubleTimeOsu { + speed_change, + adjust_pitch, + }), + Mode::Taiko => GameMod::DoubleTimeTaiko(DoubleTimeTaiko { + speed_change, + adjust_pitch, + }), + Mode::Catch => GameMod::DoubleTimeCatch(DoubleTimeCatch { + speed_change, + adjust_pitch, + }), + Mode::Mania => GameMod::DoubleTimeMania(DoubleTimeMania { + speed_change, + adjust_pitch, + }), + } + }) + } + }; + Ok(mods) + } +} + +#[derive(Debug, Clone, PartialEq, Default)] +pub struct Mods { + pub inner: GameMods, +} impl Mods { - // Return the string length of the string representation of the mods. - pub fn str_len(&self) -> usize { - let s = format!("{}", self); - s.len() + pub const NOMOD: &'static Mods = &Mods { + inner: GameMods::new(), + }; + + pub fn strip_lazer(&self, mode: Mode) -> Self { + let mut m = self.clone(); + m.inner.insert(Self::classic_mod_of(mode)); + m } + fn classic_mod_of(mode: Mode) -> rosu::GameMod { + match mode { + Mode::Std => rosu::GameMod::ClassicOsu(rosu::generated_mods::ClassicOsu::default()), + Mode::Taiko => { + rosu::GameMod::ClassicTaiko(rosu::generated_mods::ClassicTaiko::default()) + } + Mode::Catch => { + rosu::GameMod::ClassicCatch(rosu::generated_mods::ClassicCatch::default()) + } + Mode::Mania => { + rosu::GameMod::ClassicMania(rosu::generated_mods::ClassicMania::default()) + } + } + } +} + +impl From for Mods { + fn from(inner: GameMods) -> Self { + Self { inner } + } +} + +// bitflags::bitflags! { +// /// The mods available to osu! +// #[derive(std::default::Default, Serialize, Deserialize)] +// pub struct Mods: u64 { +// const NF = 1 << 0; +// const EZ = 1 << 1; +// const TD = 1 << 2; +// const HD = 1 << 3; +// const HR = 1 << 4; +// const SD = 1 << 5; +// const DT = 1 << 6; +// const RX = 1 << 7; +// const HT = 1 << 8; +// const NC = 1 << 9; +// const FL = 1 << 10; +// const AT = 1 << 11; +// const SO = 1 << 12; +// const AP = 1 << 13; +// const PF = 1 << 14; +// const KEY4 = 1 << 15; /* TODO: what are these abbreviated to? */ +// const KEY5 = 1 << 16; +// const KEY6 = 1 << 17; +// const KEY7 = 1 << 18; +// const KEY8 = 1 << 19; +// const FADEIN = 1 << 20; +// const RANDOM = 1 << 21; +// const CINEMA = 1 << 22; +// const TARGET = 1 << 23; +// const KEY9 = 1 << 24; +// const KEYCOOP = 1 << 25; +// const KEY1 = 1 << 26; +// const KEY3 = 1 << 27; +// const KEY2 = 1 << 28; +// const SCOREV2 = 1 << 29; + +// // Made up flags +// const LAZER = 1 << 59; +// const UNKNOWN = 1 << 60; +// } +// } + +// impl Mods { +// pub const NOMOD: Mods = Mods::empty(); +// pub const TOUCH_DEVICE: Mods = Self::TD; +// pub const NOVIDEO: Mods = Self::TD; /* never forget */ +// pub const SPEED_CHANGING: Mods = +// Mods::from_bits_truncate(Self::DT.bits | Self::HT.bits | Self::NC.bits); +// pub const MAP_CHANGING: Mods = +// Mods::from_bits_truncate(Self::HR.bits | Self::EZ.bits | Self::SPEED_CHANGING.bits); +// } + +// const MODS_WITH_NAMES: &[(Mods, &str)] = &[ +// (Mods::NF, "NF"), +// (Mods::EZ, "EZ"), +// (Mods::TD, "TD"), +// (Mods::HD, "HD"), +// (Mods::HR, "HR"), +// (Mods::SD, "SD"), +// (Mods::DT, "DT"), +// (Mods::RX, "RX"), +// (Mods::HT, "HT"), +// (Mods::NC, "NC"), +// (Mods::FL, "FL"), +// (Mods::AT, "AT"), +// (Mods::SO, "SO"), +// (Mods::AP, "AP"), +// (Mods::PF, "PF"), +// (Mods::KEY1, "1K"), +// (Mods::KEY2, "2K"), +// (Mods::KEY3, "3K"), +// (Mods::KEY4, "4K"), +// (Mods::KEY5, "5K"), +// (Mods::KEY6, "6K"), +// (Mods::KEY7, "7K"), +// (Mods::KEY8, "8K"), +// (Mods::KEY9, "9K"), +// (Mods::UNKNOWN, "??"), +// ]; + +impl Mods { + pub fn bits(&self) -> u32 { + self.inner.bits() + } + + pub fn contains(&self, other: &Mods) -> bool { + other + .inner + .iter() + .filter(|v| v.acronym().as_str() != "CL") + .all(|m| self.inner.contains(m)) + } // Format the mods into a string with padded size. pub fn to_string_padded(&self, size: usize) -> String { let s = format!("{}", self); let real_padded = size; format!("{:>mw$}", s, mw = real_padded) } + + /// Get details on the mods, if they are present. + pub fn details(&self) -> Vec { + use rosu::GameMod::*; + fn fmt_speed_change( + mod_name: &str, + speed_change: &Option, + adjust_pitch: &Option, + ) -> Option { + if speed_change.is_none() && adjust_pitch.is_none() { + return None; + } + let mut s = format!("**{}**: ", mod_name); + let mut need_comma = false; + if let Some(speed) = speed_change { + s += &format!("speed **{:.2}x**", speed); + need_comma = true; + } + if let Some(true) = adjust_pitch { + if need_comma { + s += ", "; + } + s += "pitch **changed**" + } + Some(s) + } + self.inner + .iter() + .filter_map(|m| match m { + DoubleTimeOsu(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch), + DoubleTimeTaiko(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch), + DoubleTimeCatch(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch), + DoubleTimeMania(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch), + NightcoreOsu(dt) => fmt_speed_change("NC", &dt.speed_change, &None), + NightcoreTaiko(dt) => fmt_speed_change("NC", &dt.speed_change, &None), + NightcoreCatch(dt) => fmt_speed_change("NC", &dt.speed_change, &None), + NightcoreMania(dt) => fmt_speed_change("NC", &dt.speed_change, &None), + HalfTimeOsu(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch), + HalfTimeTaiko(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch), + HalfTimeCatch(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch), + HalfTimeMania(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch), + DaycoreOsu(ht) => fmt_speed_change("DC", &ht.speed_change, &None), + DaycoreTaiko(ht) => fmt_speed_change("DC", &ht.speed_change, &None), + DaycoreCatch(ht) => fmt_speed_change("DC", &ht.speed_change, &None), + DaycoreMania(ht) => fmt_speed_change("DC", &ht.speed_change, &None), + + _ => None, + }) + .collect() + // let mut res: Vec = vec![]; + + // for m in &self.inner { + // match m { + // DoubleTimeOsu(dt) => + // } + // } + + // res + } } -impl std::str::FromStr for Mods { - type Err = String; - fn from_str(mut s: &str) -> Result { - let mut res = Self::default(); +impl Mods { + pub fn from_str(mut s: &str, mode: Mode) -> Result { // Strip leading + if s.starts_with('+') { s = &s[1..]; } - while s.len() >= 2 { - let (m, nw) = s.split_at(2); - s = nw; - match &m.to_uppercase()[..] { - "NF" => res |= Mods::NF, - "EZ" => res |= Mods::EZ, - "TD" => res |= Mods::TD, - "HD" => res |= Mods::HD, - "HR" => res |= Mods::HR, - "SD" => res |= Mods::SD, - "DT" => res |= Mods::DT, - "RX" => res |= Mods::RX, - "HT" => res |= Mods::HT, - "NC" => res |= Mods::NC | Mods::DT, - "FL" => res |= Mods::FL, - "AT" => res |= Mods::AT, - "SO" => res |= Mods::SO, - "AP" => res |= Mods::AP, - "PF" => res |= Mods::PF | Mods::SD, - "1K" => res |= Mods::KEY1, - "2K" => res |= Mods::KEY2, - "3K" => res |= Mods::KEY3, - "4K" => res |= Mods::KEY4, - "5K" => res |= Mods::KEY5, - "6K" => res |= Mods::KEY6, - "7K" => res |= Mods::KEY7, - "8K" => res |= Mods::KEY8, - "9K" => res |= Mods::KEY9, - "??" => res |= Mods::UNKNOWN, - v => return Err(format!("{} is not a valid mod", v)), - } - } - if !s.is_empty() { - Err("String of odd length is not a mod string".to_owned()) - } else { - Ok(res) + let intermode = + GameModsIntermode::try_from_acronyms(s).ok_or_else(|| error!("Invalid mods: {}", s))?; + let mut inner = intermode + .try_with_mode(mode.into()) + .ok_or_else(|| error!("Invalid mods for `{}`: {}", mode, intermode))?; + // Always add classic mod to `inner` + inner.insert(Self::classic_mod_of(mode)); + if !inner.is_valid() { + return Err(error!("Incompatible mods found: {}", inner)); } + Ok(Self { inner }) + // let mut res = GameModsIntermode::default(); + // while s.len() >= 2 { + // let (m, nw) = s.split_at(2); + // s = nw; + // match &m.to_uppercase()[..] { + // "NF" => res.insert(GameModIntermode::NoFail), + // "EZ" => res.insert(GameModIntermode::Easy), + // "TD" => res.insert(GameModIntermode::TouchDevice), + // "HD" => res.insert(GameModIntermode::Hidden), + // "HR" => res.insert(GameModIntermode::HardRock), + // "SD" => res.insert(GameModIntermode::SuddenDeath), + // "DT" => res.insert(GameModIntermode::DoubleTime), + // "RX" => res.insert(GameModIntermode::Relax), + // "HT" => res.insert(GameModIntermode::HalfTime), + // "NC" => res.insert(GameModIntermode::Nightcore), + // "FL" => res.insert(GameModIntermode::Flashlight), + // "AT" => res.insert(GameModIntermode::Autopilot), + // "SO" => res.insert(GameModIntermode::SpunOut), + // "AP" => res.insert(GameModIntermode::Autoplay), + // "PF" => res.insert(GameModIntermode::Perfect), + // "1K" => res.insert(GameModIntermode::OneKey), + // "2K" => res.insert(GameModIntermode::TwoKeys), + // "3K" => res.insert(GameModIntermode::ThreeKeys), + // "4K" => res.insert(GameModIntermode::FourKeys), + // "5K" => res.insert(GameModIntermode::FiveKeys), + // "6K" => res.insert(GameModIntermode::SixKeys), + // "7K" => res.insert(GameModIntermode::SevenKeys), + // "8K" => res.insert(GameModIntermode::EightKeys), + // "9K" => res.insert(GameModIntermode::NineKeys), + // v => return Err(format!("{} is not a valid mod", v)), + // } + // } + // if !s.is_empty() { + // Err("String of odd length is not a mod string".to_owned()) + // } else { + // Ok(Mods { inner: res }) + // } } } impl fmt::Display for Mods { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - if !(*self & (Mods::all() ^ Mods::LAZER)).is_empty() { - write!(f, "+")?; - for p in MODS_WITH_NAMES.iter() { - if !self.contains(p.0) { - continue; - } - match p.0 { - Mods::DT if self.contains(Mods::NC) => continue, - Mods::SD if self.contains(Mods::PF) => continue, - _ => (), - }; - write!(f, "{}", p.1)?; + let is_lazer = !self.inner.contains_intermode(GameModIntermode::Classic); + let mods = if !is_lazer { + let mut v = self.inner.clone(); + v.remove_intermode(GameModIntermode::Classic); + Cow::Owned(v) + } else { + Cow::Borrowed(&self.inner) + }; + if !mods.is_empty() { + write!(f, "+{}", mods)?; + } + if let Some(clock) = mods.clock_rate() { + if clock != 1.0 && clock != 1.5 && clock != 0.75 { + write!(f, "@{:.2}x", clock)?; } } - if self.contains(Mods::LAZER) { + if is_lazer { write!(f, "{}", LAZER_TEXT)?; } Ok(()) + // if !(*self & (Mods::all() ^ Mods::LAZER)).is_empty() { + // write!(f, "+")?; + // for p in MODS_WITH_NAMES.iter() { + // if !self.contains(p.0) { + // continue; + // } + // match p.0 { + // Mods::DT if self.contains(Mods::NC) => continue, + // Mods::SD if self.contains(Mods::PF) => continue, + // _ => (), + // }; + // write!(f, "{}", p.1)?; + // } + // } + // if self.contains(Mods::LAZER) { + // write!(f, "{}", LAZER_TEXT)?; + // } + // Ok(()) } } diff --git a/youmubot-osu/src/models/rosu.rs b/youmubot-osu/src/models/rosu.rs index dec8256..a861401 100644 --- a/youmubot-osu/src/models/rosu.rs +++ b/youmubot-osu/src/models/rosu.rs @@ -1,7 +1,4 @@ -use rosu_v2::model::{ - self as rosu, - mods::{GameModIntermode, GameModsIntermode}, -}; +use rosu_v2::model::{self as rosu}; use super::*; @@ -244,180 +241,180 @@ impl From for Rank { } } -impl From for rosu::mods::GameModsIntermode { - fn from(value: Mods) -> Self { - let mut res = GameModsIntermode::new(); - const MOD_MAP: &[(Mods, GameModIntermode)] = &[ - (Mods::NF, GameModIntermode::NoFail), - (Mods::EZ, GameModIntermode::Easy), - (Mods::TD, GameModIntermode::TouchDevice), - (Mods::HD, GameModIntermode::Hidden), - (Mods::HR, GameModIntermode::HardRock), - (Mods::SD, GameModIntermode::SuddenDeath), - (Mods::DT, GameModIntermode::DoubleTime), - (Mods::RX, GameModIntermode::Relax), - (Mods::HT, GameModIntermode::HalfTime), - (Mods::NC, GameModIntermode::Nightcore), - (Mods::FL, GameModIntermode::Flashlight), - (Mods::AT, GameModIntermode::Autoplay), - (Mods::SO, GameModIntermode::SpunOut), - (Mods::AP, GameModIntermode::Autopilot), - (Mods::PF, GameModIntermode::Perfect), - (Mods::KEY1, GameModIntermode::OneKey), - (Mods::KEY2, GameModIntermode::TwoKeys), - (Mods::KEY3, GameModIntermode::ThreeKeys), - (Mods::KEY4, GameModIntermode::FourKeys), - (Mods::KEY5, GameModIntermode::FiveKeys), - (Mods::KEY6, GameModIntermode::SixKeys), - (Mods::KEY7, GameModIntermode::SevenKeys), - (Mods::KEY8, GameModIntermode::EightKeys), - (Mods::KEY9, GameModIntermode::NineKeys), - ]; - for (m1, m2) in MOD_MAP { - if value.contains(*m1) { - res.insert(*m2); - } - } - if !value.contains(Mods::LAZER) { - res.insert(GameModIntermode::Classic); - } - res - } -} +// impl From for rosu::mods::GameModsIntermode { +// fn from(value: Mods) -> Self { +// let mut res = GameModsIntermode::new(); +// const MOD_MAP: &[(Mods, GameModIntermode)] = &[ +// (Mods::NF, GameModIntermode::NoFail), +// (Mods::EZ, GameModIntermode::Easy), +// (Mods::TD, GameModIntermode::TouchDevice), +// (Mods::HD, GameModIntermode::Hidden), +// (Mods::HR, GameModIntermode::HardRock), +// (Mods::SD, GameModIntermode::SuddenDeath), +// (Mods::DT, GameModIntermode::DoubleTime), +// (Mods::RX, GameModIntermode::Relax), +// (Mods::HT, GameModIntermode::HalfTime), +// (Mods::NC, GameModIntermode::Nightcore), +// (Mods::FL, GameModIntermode::Flashlight), +// (Mods::AT, GameModIntermode::Autoplay), +// (Mods::SO, GameModIntermode::SpunOut), +// (Mods::AP, GameModIntermode::Autopilot), +// (Mods::PF, GameModIntermode::Perfect), +// (Mods::KEY1, GameModIntermode::OneKey), +// (Mods::KEY2, GameModIntermode::TwoKeys), +// (Mods::KEY3, GameModIntermode::ThreeKeys), +// (Mods::KEY4, GameModIntermode::FourKeys), +// (Mods::KEY5, GameModIntermode::FiveKeys), +// (Mods::KEY6, GameModIntermode::SixKeys), +// (Mods::KEY7, GameModIntermode::SevenKeys), +// (Mods::KEY8, GameModIntermode::EightKeys), +// (Mods::KEY9, GameModIntermode::NineKeys), +// ]; +// for (m1, m2) in MOD_MAP { +// if value.contains(*m1) { +// res.insert(*m2); +// } +// } +// if !value.contains(Mods::LAZER) { +// res.insert(GameModIntermode::Classic); +// } +// res +// } +// } -impl From for Mods { - fn from(value: rosu_v2::prelude::GameModsIntermode) -> Self { - let init = if value.contains(GameModIntermode::Classic) { - Mods::NOMOD - } else { - Mods::LAZER - }; - value - .into_iter() - .map(|m| match m { - GameModIntermode::NoFail => Mods::NF, - GameModIntermode::Easy => Mods::EZ, - GameModIntermode::TouchDevice => Mods::TD, - GameModIntermode::Hidden => Mods::HD, - GameModIntermode::HardRock => Mods::HR, - GameModIntermode::SuddenDeath => Mods::SD, - GameModIntermode::DoubleTime => Mods::DT, - GameModIntermode::Relax => Mods::RX, - GameModIntermode::HalfTime => Mods::HT, - GameModIntermode::Nightcore => Mods::DT | Mods::NC, - GameModIntermode::Flashlight => Mods::FL, - GameModIntermode::Autoplay => Mods::AT, - GameModIntermode::SpunOut => Mods::SO, - GameModIntermode::Autopilot => Mods::AP, - GameModIntermode::Perfect => Mods::SD | Mods::PF, - GameModIntermode::OneKey => Mods::KEY1, - GameModIntermode::TwoKeys => Mods::KEY2, - GameModIntermode::ThreeKeys => Mods::KEY3, - GameModIntermode::FourKeys => Mods::KEY4, - GameModIntermode::FiveKeys => Mods::KEY5, - GameModIntermode::SixKeys => Mods::KEY6, - GameModIntermode::SevenKeys => Mods::KEY7, - GameModIntermode::EightKeys => Mods::KEY8, - GameModIntermode::NineKeys => Mods::KEY9, - GameModIntermode::Classic => Mods::NOMOD, - _ => Mods::UNKNOWN, - }) - .fold(init, |a, b| a | b) +// impl From for Mods { +// fn from(value: rosu_v2::prelude::GameModsIntermode) -> Self { +// let init = if value.contains(GameModIntermode::Classic) { +// Mods::NOMOD +// } else { +// Mods::LAZER +// }; +// value +// .into_iter() +// .map(|m| match m { +// GameModIntermode::NoFail => Mods::NF, +// GameModIntermode::Easy => Mods::EZ, +// GameModIntermode::TouchDevice => Mods::TD, +// GameModIntermode::Hidden => Mods::HD, +// GameModIntermode::HardRock => Mods::HR, +// GameModIntermode::SuddenDeath => Mods::SD, +// GameModIntermode::DoubleTime => Mods::DT, +// GameModIntermode::Relax => Mods::RX, +// GameModIntermode::HalfTime => Mods::HT, +// GameModIntermode::Nightcore => Mods::DT | Mods::NC, +// GameModIntermode::Flashlight => Mods::FL, +// GameModIntermode::Autoplay => Mods::AT, +// GameModIntermode::SpunOut => Mods::SO, +// GameModIntermode::Autopilot => Mods::AP, +// GameModIntermode::Perfect => Mods::SD | Mods::PF, +// GameModIntermode::OneKey => Mods::KEY1, +// GameModIntermode::TwoKeys => Mods::KEY2, +// GameModIntermode::ThreeKeys => Mods::KEY3, +// GameModIntermode::FourKeys => Mods::KEY4, +// GameModIntermode::FiveKeys => Mods::KEY5, +// GameModIntermode::SixKeys => Mods::KEY6, +// GameModIntermode::SevenKeys => Mods::KEY7, +// GameModIntermode::EightKeys => Mods::KEY8, +// GameModIntermode::NineKeys => Mods::KEY9, +// GameModIntermode::Classic => Mods::NOMOD, +// _ => Mods::UNKNOWN, +// }) +// .fold(init, |a, b| a | b) - // Mods::from_bits_truncate(value.bits() as u64) - } -} +// // Mods::from_bits_truncate(value.bits() as u64) +// } +// } -impl From for Mods { - fn from(value: rosu::mods::GameMods) -> Self { - let unknown = - rosu::mods::GameModIntermode::Unknown(rosu_v2::prelude::UnknownMod::default()); - value - .iter() - .cloned() - .map(|m| match m { - rosu::mods::GameMod::HalfTimeOsu(ht) - if ht.speed_change.is_some_and(|v| v != 0.75) => - { - unknown - } - rosu::mods::GameMod::DaycoreOsu(dc) - if dc.speed_change.is_some_and(|v| v != 0.75) => - { - unknown - } - rosu::mods::GameMod::DaycoreOsu(_) => rosu::mods::GameModIntermode::HalfTime, - rosu::mods::GameMod::DoubleTimeOsu(dt) - if dt.speed_change.is_some_and(|v| v != 1.5) => - { - unknown - } - rosu::mods::GameMod::NightcoreOsu(nc) - if nc.speed_change.is_some_and(|v| v != 1.5) => - { - unknown - } - rosu::mods::GameMod::HalfTimeTaiko(ht) - if ht.speed_change.is_some_and(|v| v != 0.75) => - { - unknown - } - rosu::mods::GameMod::DaycoreTaiko(dc) - if dc.speed_change.is_some_and(|v| v != 0.75) => - { - unknown - } - rosu::mods::GameMod::DaycoreTaiko(_) => rosu::mods::GameModIntermode::HalfTime, - rosu::mods::GameMod::DoubleTimeTaiko(dt) - if dt.speed_change.is_some_and(|v| v != 1.5) => - { - unknown - } - rosu::mods::GameMod::NightcoreTaiko(nc) - if nc.speed_change.is_some_and(|v| v != 1.5) => - { - unknown - } - rosu::mods::GameMod::HalfTimeCatch(ht) - if ht.speed_change.is_some_and(|v| v != 0.75) => - { - unknown - } - rosu::mods::GameMod::DaycoreCatch(dc) - if dc.speed_change.is_some_and(|v| v != 0.75) => - { - unknown - } - rosu::mods::GameMod::DaycoreCatch(_) => rosu::mods::GameModIntermode::HalfTime, - rosu::mods::GameMod::DoubleTimeCatch(dt) - if dt.speed_change.is_some_and(|v| v != 1.5) => - { - unknown - } - rosu::mods::GameMod::NightcoreCatch(nc) - if nc.speed_change.is_some_and(|v| v != 1.5) => - { - unknown - } - rosu::mods::GameMod::HalfTimeMania(ht) - if ht.speed_change.is_some_and(|v| v != 0.75) => - { - unknown - } - rosu::mods::GameMod::DaycoreMania(dc) - if dc.speed_change.is_some_and(|v| v != 0.75) => - { - unknown - } - rosu::mods::GameMod::DaycoreMania(_) => rosu::mods::GameModIntermode::HalfTime, - rosu::mods::GameMod::DoubleTimeMania(dt) - if dt.speed_change.is_some_and(|v| v != 1.5) => - { - unknown - } - _ => m.intermode(), - }) - .collect::() - .into() - } -} +// impl From for Mods { +// fn from(value: rosu::mods::GameMods) -> Self { +// let unknown = +// rosu::mods::GameModIntermode::Unknown(rosu_v2::prelude::UnknownMod::default()); +// value +// .iter() +// .cloned() +// .map(|m| match m { +// rosu::mods::GameMod::HalfTimeOsu(ht) +// if ht.speed_change.is_some_and(|v| v != 0.75) => +// { +// unknown +// } +// rosu::mods::GameMod::DaycoreOsu(dc) +// if dc.speed_change.is_some_and(|v| v != 0.75) => +// { +// unknown +// } +// rosu::mods::GameMod::DaycoreOsu(_) => rosu::mods::GameModIntermode::HalfTime, +// rosu::mods::GameMod::DoubleTimeOsu(dt) +// if dt.speed_change.is_some_and(|v| v != 1.5) => +// { +// unknown +// } +// rosu::mods::GameMod::NightcoreOsu(nc) +// if nc.speed_change.is_some_and(|v| v != 1.5) => +// { +// unknown +// } +// rosu::mods::GameMod::HalfTimeTaiko(ht) +// if ht.speed_change.is_some_and(|v| v != 0.75) => +// { +// unknown +// } +// rosu::mods::GameMod::DaycoreTaiko(dc) +// if dc.speed_change.is_some_and(|v| v != 0.75) => +// { +// unknown +// } +// rosu::mods::GameMod::DaycoreTaiko(_) => rosu::mods::GameModIntermode::HalfTime, +// rosu::mods::GameMod::DoubleTimeTaiko(dt) +// if dt.speed_change.is_some_and(|v| v != 1.5) => +// { +// unknown +// } +// rosu::mods::GameMod::NightcoreTaiko(nc) +// if nc.speed_change.is_some_and(|v| v != 1.5) => +// { +// unknown +// } +// rosu::mods::GameMod::HalfTimeCatch(ht) +// if ht.speed_change.is_some_and(|v| v != 0.75) => +// { +// unknown +// } +// rosu::mods::GameMod::DaycoreCatch(dc) +// if dc.speed_change.is_some_and(|v| v != 0.75) => +// { +// unknown +// } +// rosu::mods::GameMod::DaycoreCatch(_) => rosu::mods::GameModIntermode::HalfTime, +// rosu::mods::GameMod::DoubleTimeCatch(dt) +// if dt.speed_change.is_some_and(|v| v != 1.5) => +// { +// unknown +// } +// rosu::mods::GameMod::NightcoreCatch(nc) +// if nc.speed_change.is_some_and(|v| v != 1.5) => +// { +// unknown +// } +// rosu::mods::GameMod::HalfTimeMania(ht) +// if ht.speed_change.is_some_and(|v| v != 0.75) => +// { +// unknown +// } +// rosu::mods::GameMod::DaycoreMania(dc) +// if dc.speed_change.is_some_and(|v| v != 0.75) => +// { +// unknown +// } +// rosu::mods::GameMod::DaycoreMania(_) => rosu::mods::GameModIntermode::HalfTime, +// rosu::mods::GameMod::DoubleTimeMania(dt) +// if dt.speed_change.is_some_and(|v| v != 1.5) => +// { +// unknown +// } +// _ => m.intermode(), +// }) +// .collect::() +// .into() +// } +// } diff --git a/youmubot-osu/src/request.rs b/youmubot-osu/src/request.rs index 1ea5057..cd72cc8 100644 --- a/youmubot-osu/src/request.rs +++ b/youmubot-osu/src/request.rs @@ -1,7 +1,7 @@ use core::fmt; use crate::models::{Mode, Mods}; -use crate::Client; +use crate::OsuClient; use rosu_v2::error::OsuError; use youmubot_prelude::*; @@ -58,6 +58,7 @@ pub mod builders { use crate::models; + use super::OsuClient; use super::*; /// A builder for a Beatmap request. pub struct BeatmapRequestBuilder { @@ -82,7 +83,7 @@ pub mod builders { self } - pub(crate) async fn build(self, client: &Client) -> Result> { + pub(crate) async fn build(self, client: &OsuClient) -> Result> { Ok(match self.kind { BeatmapRequestKind::Beatmap(id) => { match handle_not_found(client.rosu.beatmap().map_id(id as u32).await)? { @@ -141,7 +142,7 @@ pub mod builders { self } - pub(crate) async fn build(self, client: &Client) -> Result> { + pub(crate) async fn build(self, client: &OsuClient) -> Result> { let mut r = client.rosu.user(self.user); if let Some(mode) = self.mode { r = r.mode(mode.into()); @@ -154,7 +155,7 @@ pub mod builders { - time::Duration::DAY * self.event_days.unwrap_or(31); let mut events = handle_not_found(client.rosu.recent_activity(user.user_id).await)? .unwrap_or(vec![]); - events.retain(|e| (now <= e.created_at)); + events.retain(|e: &rosu_v2::model::event::Event| (now <= e.created_at)); let stats = user.statistics.take().unwrap(); Ok(Some(models::User::from_rosu(user, stats, events))) } @@ -199,31 +200,29 @@ pub mod builders { self } - pub(crate) async fn build(self, client: &Client) -> Result> { + pub(crate) async fn build(self, osu: &OsuClient) -> Result> { let scores = handle_not_found(match self.user { Some(user) => { - let mut r = client - .rosu - .beatmap_user_scores(self.beatmap_id as u32, user); + let mut r = osu.rosu.beatmap_user_scores(self.beatmap_id as u32, user); if let Some(mode) = self.mode { r = r.mode(mode.into()); } match self.mods { Some(mods) => r.await.map(|mut ss| { - let mods = GameModsIntermode::from(mods); - ss.retain(|s| mods.iter().all(|m| s.mods.contains_intermode(m))); + // let mods = GameModsIntermode::from(mods.inner); + ss.retain(|s| Mods::from(s.mods.clone()).contains(&mods)); ss }), None => r.await, } } None => { - let mut r = client.rosu.beatmap_scores(self.beatmap_id as u32).global(); + let mut r = osu.rosu.beatmap_scores(self.beatmap_id as u32).global(); if let Some(mode) = self.mode { r = r.mode(mode.into()); } if let Some(mods) = self.mods { - r = r.mods(GameModsIntermode::from(mods)); + r = r.mods(GameModsIntermode::from(mods.inner)); } if let Some(limit) = self.limit { r = r.limit(limit as u32); @@ -268,7 +267,7 @@ pub mod builders { self } - pub(crate) async fn build(self, client: &Client) -> Result> { + pub(crate) async fn build(self, client: &OsuClient) -> Result> { let scores = handle_not_found({ let mut r = client.rosu.user_scores(self.user); r = match self.score_type { diff --git a/youmubot-prelude/src/announcer.rs b/youmubot-prelude/src/announcer.rs index 71377bc..871ad92 100644 --- a/youmubot-prelude/src/announcer.rs +++ b/youmubot-prelude/src/announcer.rs @@ -142,7 +142,6 @@ impl AnnouncerHandler { } pub fn run(self, client: &Client) -> AnnouncerRunner { - AnnouncerRunner { cache_http: CacheAndHttp::from_client(client), data: client.data.clone(), diff --git a/youmubot-prelude/src/setup.rs b/youmubot-prelude/src/setup.rs index 6624882..43a084c 100644 --- a/youmubot-prelude/src/setup.rs +++ b/youmubot-prelude/src/setup.rs @@ -39,8 +39,6 @@ pub async fn setup_prelude( // Set up the SQL client. data.insert::(sql_pool.clone()); - - Env { http: http_client, sql: sql_pool,