diff --git a/youmubot-osu/src/discord/commands.rs b/youmubot-osu/src/discord/commands.rs index 27b1177..00fc31c 100644 --- a/youmubot-osu/src/discord/commands.rs +++ b/youmubot-osu/src/discord/commands.rs @@ -1,14 +1,26 @@ +use std::cmp::Ordering; + use super::*; +use cache::save_beatmap; use display::display_beatmapset; use embeds::ScoreEmbedBuilder; use link_parser::EmbedType; -use poise::CreateReply; +use poise::{ChoiceParameter, CreateReply}; use serenity::all::User; /// osu!-related command group. #[poise::command( slash_command, - subcommands("profile", "top", "recent", "pinned", "save", "forcesave", "beatmap") + subcommands( + "profile", + "top", + "recent", + "pinned", + "save", + "forcesave", + "beatmap", + "check" + ) )] pub async fn osu(_ctx: CmdContext<'_, U>) -> Result<()> { Ok(()) @@ -308,20 +320,7 @@ async fn beatmap( ctx.defer().await?; - let beatmap = parse_map_input(ctx.channel_id(), env, map, mode).await?; - - // override into beatmapset if needed - let beatmap = if beatmapset == Some(true) { - match beatmap { - EmbedType::Beatmap(beatmap, _, _) => { - let beatmaps = env.beatmaps.get_beatmapset(beatmap.beatmapset_id).await?; - EmbedType::Beatmapset(beatmaps) - } - bm @ EmbedType::Beatmapset(_) => bm, - } - } else { - beatmap - }; + let beatmap = parse_map_input(ctx.channel_id(), env, map, mode, beatmapset).await?; // override mods and mode if needed match beatmap { @@ -361,6 +360,8 @@ async fn beatmap( )]), ) .await?; + let bmode = beatmap.mode.with_override(mode); + save_beatmap(env, ctx.channel_id(), &BeatmapWithMode(beatmap, bmode)).await?; } EmbedType::Beatmapset(vec) => { let b0 = &vec[0]; @@ -389,6 +390,138 @@ async fn beatmap( Ok(()) } +#[derive(Debug, Clone, Copy, PartialEq, Eq, ChoiceParameter, Default)] +enum SortScoreBy { + #[default] + PP, + Score, + #[name = "Maximum Combo"] + Combo, + #[name = "Miss Count"] + Miss, + Accuracy, +} + +impl SortScoreBy { + fn compare(self, a: &Score, b: &Score) -> Ordering { + match self { + SortScoreBy::PP => { + b.pp.unwrap_or(0.0) + .partial_cmp(&a.pp.unwrap_or(0.0)) + .unwrap() + } + SortScoreBy::Score => b.normalized_score.cmp(&a.normalized_score), + SortScoreBy::Combo => b.max_combo.cmp(&a.max_combo), + SortScoreBy::Miss => b.count_miss.cmp(&a.count_miss), + SortScoreBy::Accuracy => b.server_accuracy.partial_cmp(&a.server_accuracy).unwrap(), + } + } +} + +/// Display your or a player's scores for a certain beatmap/beatmapset. +#[poise::command(slash_command)] +async fn check( + ctx: CmdContext<'_, U>, + #[description = "A link or shortlink to the beatmap or beatmapset"] map: Option, + #[description = "osu! username"] username: Option, + #[description = "Discord username"] discord_name: Option, + #[description = "Sort scores by"] sort: Option, + #[description = "Reverse the sorting order"] reverse: Option, + #[description = "Filter the mods on the scores"] mods: Option, + #[description = "Filter the mode of the scores"] mode: Option, + #[description = "Find all scores in the beatmapset instead"] beatmapset: Option, + #[description = "Score listing style"] style: Option, +) -> Result<()> { + let env = ctx.data().osu_env(); + + let user = arg_from_username_or_discord(username, discord_name); + let args = ListingArgs::from_params( + env, + None, + style.unwrap_or(ScoreListStyle::Grid), + mode, + user, + ctx.author().id, + ) + .await?; + + ctx.defer().await?; + + let embed = parse_map_input(ctx.channel_id(), env, map, mode, beatmapset).await?; + let beatmaps = match embed { + EmbedType::Beatmap(beatmap, _, _) => { + let nmode = beatmap.mode.with_override(mode); + vec![BeatmapWithMode(*beatmap, nmode)] + } + EmbedType::Beatmapset(vec) => match mode { + None => { + let default_mode = vec[0].mode; + vec.into_iter() + .filter(|b| b.mode == default_mode) + .map(|b| BeatmapWithMode(b, default_mode)) + .collect() + } + Some(m) => vec + .into_iter() + .filter(|b| b.mode == Mode::Std || b.mode == m) + .map(|b| BeatmapWithMode(b, m)) + .collect(), + }, + }; + + let display = if beatmaps.len() == 1 { + format!( + "[{}](<{}>)", + beatmaps[0].0.short_link(None, Mods::NOMOD), + beatmaps[0].0.link() + ) + } else { + format!( + "[/s/{}](<{}>)", + beatmaps[0].0.beatmapset_id, + beatmaps[0].0.beatmapset_link() + ) + }; + + let ordering = sort.unwrap_or_default(); + let mut scores = do_check(env, &beatmaps, mods, &args.user).await?; + if scores.is_empty() { + ctx.reply(format!( + "No plays found for {} on {} with the required criteria.", + args.user.mention(), + display + )) + .await?; + return Ok(()); + } + scores.sort_unstable_by(|a, b| ordering.compare(a, b)); + if reverse == Some(true) { + scores.reverse(); + } + + let msg = ctx + .clone() + .reply(format!( + "Here are the plays by {} on {}!", + args.user.mention(), + display + )) + .await? + .into_message() + .await?; + args.style + .display_scores( + scores, + beatmaps[0].1, + ctx.serenity_context(), + ctx.guild_id(), + msg, + ) + .await?; + + Ok(()) +} + fn arg_from_username_or_discord( username: Option, discord_name: Option, @@ -405,8 +538,9 @@ async fn parse_map_input( env: &OsuEnv, input: Option, mode: Option, + beatmapset: Option, ) -> Result { - Ok(match input { + let output = match input { None => { let Some((BeatmapWithMode(b, mode), bmmods)) = load_beatmap(env, channel_id, None as Option<&'_ Message>).await @@ -452,5 +586,20 @@ async fn parse_map_input( }; results.embed } - }) + }; + + // override into beatmapset if needed + let output = if beatmapset == Some(true) { + match output { + EmbedType::Beatmap(beatmap, _, _) => { + let beatmaps = env.beatmaps.get_beatmapset(beatmap.beatmapset_id).await?; + EmbedType::Beatmapset(beatmaps) + } + bm @ EmbedType::Beatmapset(_) => bm, + } + } else { + output + }; + + Ok(output) } diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index 35fbc2e..4f6c021 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -194,7 +194,7 @@ mod scores { } } - const ITEMS_PER_PAGE: usize = 5; + const ITEMS_PER_PAGE: usize = 10; #[async_trait] impl pagination::Paginate for Paginate { diff --git a/youmubot-osu/src/discord/interaction.rs b/youmubot-osu/src/discord/interaction.rs index c92b818..54698d2 100644 --- a/youmubot-osu/src/discord/interaction.rs +++ b/youmubot-osu/src/discord/interaction.rs @@ -79,7 +79,7 @@ pub fn handle_check_button<'a>( }; let header = UserHeader::from(user.clone()); - let scores = super::do_check(&env, &bm, Mods::NOMOD, &header).await?; + let scores = super::do_check(&env, &vec![bm.clone()], None, &header).await?; if scores.is_empty() { comp.create_followup( &ctx, diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index cf6705c..2dbe38b 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -21,7 +21,7 @@ use db::{OsuLastBeatmap, OsuSavedUsers, OsuUser, OsuUserMode}; use embeds::{beatmap_embed, score_embed, user_embed}; pub use hook::{dot_osu_hook, hook, score_hook}; use server_rank::{SERVER_RANK_COMMAND, SHOW_LEADERBOARD_COMMAND}; -use stream::FuturesOrdered; +use stream::{FuturesOrdered, FuturesUnordered}; use youmubot_prelude::announcer::AnnouncerHandler; use youmubot_prelude::*; @@ -925,18 +925,15 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul } }; let mode = bm.1; - let mods = args - .find::() - .ok() - .unwrap_or_default() - .to_mods(mode)?; + let umods = args.find::().ok(); + let mods = umods.clone().unwrap_or_default().to_mods(mode)?; let style = args .single::() .unwrap_or(ScoreListStyle::Grid); let username_arg = args.single::().ok(); let (_, user) = user_header_from_args(username_arg, &env, msg).await?; - let scores = do_check(&env, &bm, &mods, &user).await?; + let scores = do_check(&env, &vec![bm.clone()], umods, &user).await?; if scores.is_empty() { msg.reply(&ctx, "No scores found").await?; @@ -961,19 +958,29 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul pub(crate) async fn do_check( env: &OsuEnv, - bm: &BeatmapWithMode, - mods: &Mods, + bm: &[BeatmapWithMode], + mods: Option, user: &UserHeader, ) -> Result> { - let BeatmapWithMode(b, m) = bm; - let osu_client = &env.client; - let mut scores = osu_client - .scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(*m)) + let mut scores = bm + .iter() + .map(|bm| { + let BeatmapWithMode(b, m) = bm; + let mods = mods.clone().and_then(|t| t.to_mods(*m).ok()); + osu_client + .scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(*m)) + .map_ok(move |mut v| { + v.retain(|s| mods.as_ref().is_none_or(|m| m.contains(&s.mods))); + v + }) + }) + .collect::>() + .try_collect::>() .await? .into_iter() - .filter(|s| s.mods.contains(mods)) + .flatten() .collect::>(); scores.sort_by(|a, b| { b.pp.unwrap_or(-1.0)