diff --git a/youmubot-osu/src/discord/embeds.rs b/youmubot-osu/src/discord/embeds.rs index 94f1a36..aec5282 100644 --- a/youmubot-osu/src/discord/embeds.rs +++ b/youmubot-osu/src/discord/embeds.rs @@ -1,5 +1,8 @@ use super::BeatmapWithMode; -use crate::models::{Beatmap, Mode, Rank, Score, User}; +use crate::{ + discord::oppai_cache::BeatmapInfo, + models::{Beatmap, Mode, Mods, Rank, Score, User}, +}; use chrono::Utc; use serenity::{builder::CreateEmbed, utils::MessageBuilder}; use youmubot_prelude::*; @@ -12,7 +15,32 @@ fn format_mode(actual: Mode, original: Mode) -> String { } } -pub fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a mut CreateEmbed { +pub fn beatmap_embed<'a>( + b: &'_ Beatmap, + m: Mode, + mods: Mods, + info: Option, + c: &'a mut CreateEmbed, +) -> &'a mut CreateEmbed { + let mod_str = if mods == Mods::NOMOD { + "".to_owned() + } else { + format!(" {}", mods) + }; + let total_length = if mods.intersects(Mods::DT | Mods::NC) { + b.total_length * 2 / 3 + } else if mods.intersects(Mods::HT) { + b.total_length * 4 / 3 + } else { + b.total_length + }; + let drain_length = if mods.intersects(Mods::DT | Mods::NC) { + b.drain_length * 2 / 3 + } else if mods.intersects(Mods::HT) { + b.drain_length * 4 / 3 + } else { + b.drain_length + }; c.title( MessageBuilder::new() .push_bold_safe(&b.artist) @@ -21,6 +49,7 @@ pub fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a .push(" [") .push_bold_safe(&b.difficulty_name) .push("]") + .push(&mod_str) .build(), ) .author(|a| { @@ -34,15 +63,29 @@ pub fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a .color(0xffb6c1) .field( "Star Difficulty", - format!("{:.2}⭐", b.difficulty.stars), + format!( + "{:.2}⭐", + info.map(|v| v.stars as f64).unwrap_or(b.difficulty.stars) + ), false, ) + .fields(info.map(|info| { + ( + "Calculated pp", + format!( + "95%: **{:.2}**pp, 98%: **{:.2}**pp, 99%: **{:.2}**pp, 100%: **{:.2}**pp", + info.pp[0], info.pp[1], info.pp[2], info.pp[3] + ), + false, + ) + })) + .fields(Some(("Mods", mods, false)).filter(|_| mods != Mods::NOMOD)) .field( "Length", MessageBuilder::new() - .push_bold_safe(Duration(b.total_length)) + .push_bold_safe(Duration(total_length)) .push(" (") - .push_bold_safe(Duration(b.drain_length)) + .push_bold_safe(Duration(drain_length)) .push(" drain)") .build(), false, @@ -90,6 +133,12 @@ pub fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a .push_bold(&b.genre) .build(), ) + .footer(|f| { + if info.is_none() && mods != Mods::NOMOD { + f.text("Star difficulty not reflecting mods applied."); + } + f + }) } const MAX_DIFFS: usize = 25 - 4; diff --git a/youmubot-osu/src/discord/hook.rs b/youmubot-osu/src/discord/hook.rs index 62c2bcf..51c9ecc 100644 --- a/youmubot-osu/src/discord/hook.rs +++ b/youmubot-osu/src/discord/hook.rs @@ -1,6 +1,7 @@ use super::OsuClient; use crate::{ - models::{Beatmap, Mode}, + discord::oppai_cache::{BeatmapCache, BeatmapInfo}, + models::{Beatmap, Mode, Mods}, request::BeatmapRequestKind, }; use lazy_static::lazy_static; @@ -11,6 +12,7 @@ use serenity::{ model::channel::Message, utils::MessageBuilder, }; +use std::str::FromStr; use youmubot_prelude::*; use super::embeds::{beatmap_embed, beatmapset_embed}; @@ -34,13 +36,13 @@ pub fn hook(ctx: &mut Context, msg: &Message) -> () { let mut last_beatmap = None; for l in old_links.into_iter().chain(new_links.into_iter()) { if let Err(v) = msg.channel_id.send_message(&ctx, |m| match l.embed { - EmbedType::Beatmap(b) => { - let t = handle_beatmap(&b, l.link, l.mode, l.mods, m); + EmbedType::Beatmap(b, info, mods) => { + let t = handle_beatmap(&b, info, l.link, l.mode, mods, m); let mode = l.mode.unwrap_or(b.mode); last_beatmap = Some(super::BeatmapWithMode(b, mode)); t } - EmbedType::Beatmapset(b) => handle_beatmapset(b, l.link, l.mode, l.mods, m), + EmbedType::Beatmapset(b) => handle_beatmapset(b, l.link, l.mode, m), }) { println!("Error in osu! hook: {:?}", v) } @@ -59,7 +61,7 @@ pub fn hook(ctx: &mut Context, msg: &Message) -> () { } enum EmbedType { - Beatmap(Beatmap), + Beatmap(Beatmap, Option, Mods), Beatmapset(Vec), } @@ -67,12 +69,12 @@ struct ToPrint<'a> { embed: EmbedType, link: &'a str, mode: Option, - mods: Option<&'a str>, } fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result>, Error> { let osu = ctx.data.get_cloned::(); let mut to_prints: Vec> = Vec::new(); + let cache = ctx.data.get_cloned::(); for capture in OLD_LINK_REGEX.captures_iter(content) { let req_type = capture.name("link_type").unwrap().as_str(); let req = match req_type { @@ -100,11 +102,22 @@ fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result { for b in beatmaps.into_iter() { + // collect beatmap info + let mods = capture + .name("mods") + .map(|v| Mods::from_str(v.as_str()).ok()) + .flatten() + .unwrap_or(Mods::NOMOD); + let info = mode.unwrap_or(b.mode).to_oppai_mode().and_then(|mode| { + cache + .get_beatmap(b.beatmap_id) + .and_then(|b| b.get_info_with(Some(mode), mods)) + .ok() + }); to_prints.push(ToPrint { - embed: EmbedType::Beatmap(b), + embed: EmbedType::Beatmap(b, info, mods), link: capture.get(0).unwrap().as_str(), mode, - mods: capture.name("mods").map(|v| v.as_str()), }) } } @@ -112,7 +125,6 @@ fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result (), } @@ -123,6 +135,7 @@ fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result(ctx: &mut Context, content: &'a str) -> Result>, Error> { let osu = ctx.data.get_cloned::(); let mut to_prints: Vec> = Vec::new(); + let cache = ctx.data.get_cloned::(); for capture in NEW_LINK_REGEX.captures_iter(content) { let mode = capture.name("mode").and_then(|v| { Some(match v.as_str() { @@ -133,7 +146,6 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result return None, }) }); - let mods = capture.name("mods").map(|v| v.as_str()); let link = capture.get(0).unwrap().as_str(); let req = match capture.name("beatmap_id") { Some(ref v) => BeatmapRequestKind::Beatmap(v.as_str().parse()?), @@ -148,10 +160,24 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result { for beatmap in beatmaps.into_iter() { + // collect beatmap info + let mods = capture + .name("mods") + .map(|v| Mods::from_str(v.as_str()).ok()) + .flatten() + .unwrap_or(Mods::NOMOD); + let info = mode + .unwrap_or(beatmap.mode) + .to_oppai_mode() + .and_then(|mode| { + cache + .get_beatmap(beatmap.beatmap_id) + .and_then(|b| b.get_info_with(Some(mode), mods)) + .ok() + }); to_prints.push(ToPrint { - embed: EmbedType::Beatmap(beatmap), + embed: EmbedType::Beatmap(beatmap, info, mods), link, - mods, mode, }) } @@ -159,7 +185,6 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result to_prints.push(ToPrint { embed: EmbedType::Beatmapset(beatmaps), link, - mods, mode, }), } @@ -169,9 +194,10 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result( beatmap: &Beatmap, + info: Option, link: &'_ str, mode: Option, - mods: Option<&'_ str>, + mods: Mods, m: &'a mut CreateMessage<'b>, ) -> &'a mut CreateMessage<'b> { m.content( @@ -180,14 +206,13 @@ fn handle_beatmap<'a, 'b>( .push_mono_safe(link) .build(), ) - .embed(|b| beatmap_embed(beatmap, mode.unwrap_or(beatmap.mode), b)) + .embed(|b| beatmap_embed(beatmap, mode.unwrap_or(beatmap.mode), mods, info, b)) } fn handle_beatmapset<'a, 'b>( beatmaps: Vec, link: &'_ str, mode: Option, - mods: Option<&'_ str>, m: &'a mut CreateMessage<'b>, ) -> &'a mut CreateMessage<'b> { let mut beatmaps = beatmaps; diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index 22f5a58..93bea84 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -1,5 +1,6 @@ use crate::{ - models::{Beatmap, Mode, Score, User}, + discord::oppai_cache::BeatmapCache, + models::{Beatmap, Mode, Mods, Score, User}, request::{BeatmapRequestKind, UserID}, Client as OsuHttpClient, }; @@ -20,7 +21,7 @@ mod cache; mod db; pub(crate) mod embeds; mod hook; -mod oppai_cache; +pub(crate) mod oppai_cache; mod server_rank; use db::OsuUser; @@ -334,18 +335,26 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult #[command] #[description = "Show information from the last queried beatmap."] -#[num_args(0)] -pub fn last(ctx: &mut Context, msg: &Message, _: Args) -> CommandResult { +#[usage = "[mods = no mod]"] +#[max_args(1)] +pub fn last(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult { let b = cache::get_beatmap(&*ctx.data.read(), msg.channel_id)?; match b { Some(BeatmapWithMode(b, m)) => { + let mods = args.find::().unwrap_or(Mods::NOMOD); + let info = ctx + .data + .get_cloned::() + .get_beatmap(b.beatmap_id)? + .get_info_with(m.to_oppai_mode(), mods) + .ok(); msg.channel_id.send_message(&ctx, |f| { f.content(format!( "{}: here is the beatmap you requested!", msg.author )) - .embed(|c| beatmap_embed(&b, m, c)) + .embed(|c| beatmap_embed(&b, m, mods, info, c)) })?; } None => { diff --git a/youmubot-osu/src/discord/oppai_cache.rs b/youmubot-osu/src/discord/oppai_cache.rs index 11b3a2e..60cf6b3 100644 --- a/youmubot-osu/src/discord/oppai_cache.rs +++ b/youmubot-osu/src/discord/oppai_cache.rs @@ -12,8 +12,8 @@ pub struct BeatmapContent { /// the output of "one" oppai run. #[derive(Clone, Copy, Debug)] pub struct BeatmapInfo { - stars: f32, - pp: [f32; 4], // 95, 98, 99, 100 + pub stars: f32, + pub pp: [f32; 4], // 95, 98, 99, 100 } impl BeatmapContent { @@ -22,21 +22,27 @@ impl BeatmapContent { &self, combo: oppai_rs::Combo, accuracy: f32, + mode: Option, mods: impl Into, ) -> Result { - Ok(oppai_rs::Oppai::new_from_content(&self.content[..])? - .combo(combo)? - .accuracy(accuracy)? - .mods(mods.into()) - .pp()) + let mut oppai = oppai_rs::Oppai::new_from_content(&self.content[..])?; + oppai.combo(combo)?.accuracy(accuracy)?.mods(mods.into()); + if let Some(mode) = mode { + oppai.mode(mode)?; + } + Ok(oppai.pp()) } /// Get info given mods. pub fn get_info_with( &self, + mode: Option, mods: impl Into, ) -> Result { let mut oppai = oppai_rs::Oppai::new_from_content(&self.content[..])?; + if let Some(mode) = mode { + oppai.mode(mode)?; + } oppai.mods(mods.into()).combo(oppai_rs::Combo::PERFECT)?; let pp = [ oppai.accuracy(95.0)?.pp(), @@ -50,6 +56,7 @@ impl BeatmapContent { } /// A central cache for the beatmaps. +#[derive(Clone, Debug)] pub struct BeatmapCache { client: reqwest::blocking::Client, cache: Arc>, @@ -67,7 +74,7 @@ impl BeatmapCache { fn download_beatmap(&self, id: u64) -> Result { let content = self .client - .get(&format!("https://osu.ppy.sh/u/{}", id)) + .get(&format!("https://osu.ppy.sh/osu/{}", id)) .send()? .bytes()?; Ok(BeatmapContent { diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index 57697bf..75eaad6 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -118,6 +118,17 @@ impl fmt::Display for Mode { } } +impl Mode { + /// Convert to oppai mode. + pub fn to_oppai_mode(self) -> Option { + Some(match self { + Mode::Std => oppai_rs::Mode::Std, + Mode::Taiko => oppai_rs::Mode::Taiko, + _ => return None, + }) + } +} + #[derive(Clone, Debug, Serialize, Deserialize)] pub struct Beatmap { // Beatmapset info