diff --git a/youmubot-db-sql/migrations/20250220161301_make_last_beatmap_mode_nullable.sql b/youmubot-db-sql/migrations/20250220161301_make_last_beatmap_mode_nullable.sql new file mode 100644 index 0000000..7078dad --- /dev/null +++ b/youmubot-db-sql/migrations/20250220161301_make_last_beatmap_mode_nullable.sql @@ -0,0 +1,6 @@ +-- Add migration script here + +ALTER TABLE osu_last_beatmaps RENAME COLUMN mode TO mode_old; +ALTER TABLE osu_last_beatmaps ADD COLUMN mode INT NULL; +UPDATE osu_last_beatmaps SET mode = mode_old; +ALTER TABLE osu_last_beatmaps DROP COLUMN mode_old; diff --git a/youmubot-db-sql/src/models/osu.rs b/youmubot-db-sql/src/models/osu.rs index ce8c9e5..8d1b7fe 100644 --- a/youmubot-db-sql/src/models/osu.rs +++ b/youmubot-db-sql/src/models/osu.rs @@ -3,7 +3,7 @@ use crate::models::*; pub struct LastBeatmap { pub channel_id: i64, pub beatmap: Vec, - pub mode: u8, + pub mode: Option, } impl LastBeatmap { diff --git a/youmubot-osu/src/discord/announcer.rs b/youmubot-osu/src/discord/announcer.rs index 099f12c..d44a6a9 100644 --- a/youmubot-osu/src/discord/announcer.rs +++ b/youmubot-osu/src/discord/announcer.rs @@ -304,7 +304,7 @@ impl<'a> CollectedScore<'a> { .get_beatmap_default(self.score.beatmap_id) .await?; let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?; - Ok((BeatmapWithMode(beatmap, self.mode), content)) + Ok((BeatmapWithMode(beatmap, Some(self.mode)), content)) } async fn send_message_to( diff --git a/youmubot-osu/src/discord/commands.rs b/youmubot-osu/src/discord/commands.rs index 4b4890d..478c31e 100644 --- a/youmubot-osu/src/discord/commands.rs +++ b/youmubot-osu/src/discord/commands.rs @@ -7,6 +7,7 @@ use embeds::ScoreEmbedBuilder; use link_parser::EmbedType; use poise::{ChoiceParameter, CreateReply}; use serenity::all::User; +use server_rank::get_leaderboard_from_embed; /// osu!-related command group. #[poise::command( @@ -260,7 +261,7 @@ async fn handle_listing( let beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?; let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?; - let beatmap = BeatmapWithMode(beatmap, mode); + let beatmap = BeatmapWithMode(beatmap, Some(mode)); ctx.send({ CreateReply::default() @@ -297,7 +298,7 @@ async fn handle_listing( .into_message() .await?; style - .display_scores(plays, mode, ctx.serenity_context(), ctx.guild_id(), reply) + .display_scores(plays, ctx.serenity_context(), ctx.guild_id(), reply) .await?; } } @@ -321,24 +322,24 @@ async fn beatmap( // override mods and mode if needed match beatmap { - EmbedType::Beatmap(beatmap, info, bmmods) => { - let (beatmap, info, mods) = if mods.is_none() && mode.is_none_or(|v| v == beatmap.mode) - { - (*beatmap, info, bmmods) - } else { - let mode = mode.unwrap_or(beatmap.mode); - let mods = match mods { - None => bmmods, - Some(mods) => mods.to_mods(mode)?, + EmbedType::Beatmap(beatmap, bmode, info, bmmods) => { + let (beatmap, info, mods) = + if mods.is_none() && mode.is_none_or(|v| v == bmode.unwrap_or(beatmap.mode)) { + (*beatmap, info, bmmods) + } else { + let mode = bmode.unwrap_or(beatmap.mode); + let mods = match mods { + None => bmmods, + Some(mods) => mods.to_mods(mode)?, + }; + let beatmap = env.beatmaps.get_beatmap(beatmap.beatmap_id, mode).await?; + let info = env + .oppai + .get_beatmap(beatmap.beatmap_id) + .await? + .get_possible_pp_with(mode, &mods); + (beatmap, info, mods) }; - let beatmap = env.beatmaps.get_beatmap(beatmap.beatmap_id, mode).await?; - let info = env - .oppai - .get_beatmap(beatmap.beatmap_id) - .await? - .get_possible_pp_with(mode, &mods); - (beatmap, info, mods) - }; ctx.send( CreateReply::default() .content(format!("Information for {}", beatmap.mention(mode, &mods))) @@ -355,9 +356,14 @@ async fn beatmap( ) .await?; let bmode = beatmap.mode.with_override(mode); - save_beatmap(env, ctx.channel_id(), &BeatmapWithMode(beatmap, bmode)).await?; + save_beatmap( + env, + ctx.channel_id(), + &BeatmapWithMode(beatmap, Some(bmode)), + ) + .await?; } - EmbedType::Beatmapset(vec) => { + EmbedType::Beatmapset(vec, _) => { let b0 = &vec[0]; let msg = ctx .clone() @@ -438,35 +444,11 @@ async fn check( 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 { - beatmaps[0].0.mention(None, Mods::NOMOD) - } else { - beatmaps[0].0.beatmapset_mention() - }; + let display = embed.mention(); let ordering = sort.unwrap_or_default(); - let mut scores = do_check(env, &beatmaps, mods, &args.user).await?; + let mut scores = do_check(env, &embed, mods, &args.user).await?; if scores.is_empty() { ctx.reply(format!( "No plays found for {} on {} with the required criteria.", @@ -499,13 +481,7 @@ async fn check( }); style - .display_scores( - scores, - beatmaps[0].1, - ctx.serenity_context(), - ctx.guild_id(), - msg, - ) + .display_scores(scores, ctx.serenity_context(), ctx.guild_id(), msg) .await?; Ok(()) @@ -552,23 +528,20 @@ async fn leaderboard( let env = ctx.data().osu_env(); let guild = ctx.partial_guild().await.unwrap(); let style = style.unwrap_or_default(); + let order = sort.unwrap_or_default(); - let bm = match parse_map_input(ctx.channel_id(), env, map, mode, None).await? { - EmbedType::Beatmap(beatmap, _, _) => { - let nmode = beatmap.mode.with_override(mode); - BeatmapWithMode(*beatmap, nmode) - } - EmbedType::Beatmapset(_) => return Err(Error::msg("invalid map link")), - }; + let embed = parse_map_input(ctx.channel_id(), env, map, mode, None).await?; ctx.defer().await?; - let mut scores = server_rank::get_leaderboard( + let scoreboard_msg = embed.mention(); + let (mut scores, show_diff) = get_leaderboard_from_embed( ctx.serenity_context(), - env, - &bm, - unranked.unwrap_or(false), - sort.unwrap_or(server_rank::OrderBy::PP), + &env, + embed, + None, + unranked.unwrap_or(true), + order, guild.id, ) .await?; @@ -576,12 +549,10 @@ async fn leaderboard( scores.reverse(); } - let beatmap = &bm.0; if scores.is_empty() { ctx.reply(format!( "No scores have been recorded in **{}** on {}.", - guild.name, - beatmap.mention(mode, Mods::NOMOD), + guild.name, scoreboard_msg, )) .await?; return Ok(()); @@ -589,8 +560,7 @@ async fn leaderboard( let header = format!( "Here are the top scores of **{}** on {}", - guild.name, - beatmap.mention(mode, Mods::NOMOD), + guild.name, scoreboard_msg, ); match style { @@ -600,7 +570,7 @@ async fn leaderboard( ctx.serenity_context(), reply, scores, - &bm, + show_diff, sort.unwrap_or_default(), ) .await?; @@ -610,7 +580,6 @@ async fn leaderboard( style .display_scores( scores.into_iter().map(|s| s.score).collect(), - bm.1, ctx.serenity_context(), Some(guild.id), reply, @@ -659,18 +628,10 @@ async fn parse_map_input( ) -> Result { let output = match input { None => { - let Some((BeatmapWithMode(b, mode), bmmods)) = - load_beatmap(env, channel_id, None as Option<&'_ Message>).await - else { + let Some(v) = load_beatmap_from_channel(env, channel_id).await else { return Err(Error::msg("no beatmap mentioned in this channel")); }; - let mods = bmmods.unwrap_or_else(|| Mods::NOMOD.clone()); - let info = env - .oppai - .get_beatmap(b.beatmap_id) - .await? - .get_possible_pp_with(mode, &mods); - EmbedType::Beatmap(Box::new(b), info, mods) + v } Some(map) => { if let Ok(id) = map.parse::() { @@ -685,6 +646,7 @@ async fn parse_map_input( .get_possible_pp_with(beatmap.mode, Mods::NOMOD); return Ok(EmbedType::Beatmap( Box::new(beatmap), + None, info, Mods::NOMOD.clone(), )); @@ -708,14 +670,14 @@ async fn parse_map_input( // override into beatmapset if needed let output = if beatmapset == Some(true) { match output { - EmbedType::Beatmap(beatmap, _, _) => { + EmbedType::Beatmap(beatmap, _, _, _) => { let beatmaps = env .beatmaps .get_beatmapset(beatmap.beatmapset_id, mode) .await?; - EmbedType::Beatmapset(beatmaps) + EmbedType::Beatmapset(beatmaps, mode) } - bm @ EmbedType::Beatmapset(_) => bm, + bm @ EmbedType::Beatmapset(_, _) => bm, } } else { output diff --git a/youmubot-osu/src/discord/db.rs b/youmubot-osu/src/discord/db.rs index efe640a..8896bbe 100644 --- a/youmubot-osu/src/discord/db.rs +++ b/youmubot-osu/src/discord/db.rs @@ -81,11 +81,17 @@ impl OsuLastBeatmap { } impl OsuLastBeatmap { - pub async fn by_channel(&self, id: impl Into) -> Result> { + pub async fn by_channel( + &self, + id: impl Into, + ) -> Result)>> { let last_beatmap = models::LastBeatmap::by_channel_id(id.into().get() as i64, &self.0).await?; Ok(match last_beatmap { - Some(lb) => Some((bincode::deserialize(&lb.beatmap[..])?, lb.mode.into())), + Some(lb) => Some(( + bincode::deserialize(&lb.beatmap[..])?, + lb.mode.map(|s| s.into()), + )), None => None, }) } @@ -94,12 +100,12 @@ impl OsuLastBeatmap { &self, channel: impl Into, beatmap: &Beatmap, - mode: Mode, + mode: Option, ) -> Result<()> { let b = models::LastBeatmap { channel_id: channel.into().get() as i64, beatmap: bincode::serialize(beatmap)?, - mode: mode as u8, + mode: mode.map(|mode| mode as u8), }; b.store(&self.0).await?; Ok(()) diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index 35fbc2e..1cfc51d 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -7,7 +7,7 @@ mod scores { use youmubot_prelude::*; - use crate::models::{Mode, Score}; + use crate::models::Score; #[derive(Debug, Clone, Copy, PartialEq, Eq, ChoiceParameter)] /// The style for the scores list to be displayed. @@ -40,16 +40,13 @@ mod scores { pub async fn display_scores( self, scores: Vec, - mode: Mode, ctx: &Context, guild_id: Option, m: Message, ) -> Result<()> { match self { - ScoreListStyle::Table => table::display_scores_table(scores, mode, ctx, m).await, - ScoreListStyle::Grid => { - grid::display_scores_grid(scores, mode, ctx, guild_id, m).await - } + ScoreListStyle::Table => table::display_scores_table(scores, ctx, m).await, + ScoreListStyle::Grid => grid::display_scores_grid(scores, ctx, guild_id, m).await, } } } @@ -64,11 +61,10 @@ mod scores { use crate::discord::interaction::score_components; use crate::discord::{cache::save_beatmap, BeatmapWithMode, OsuEnv}; - use crate::models::{Mode, Score}; + use crate::models::Score; pub async fn display_scores_grid( scores: Vec, - mode: Mode, ctx: &Context, guild_id: Option, mut on: Message, @@ -80,11 +76,7 @@ mod scores { } paginate_with_first_message( - Paginate { - scores, - guild_id, - mode, - }, + Paginate { scores, guild_id }, ctx, on, std::time::Duration::from_secs(60), @@ -96,7 +88,6 @@ mod scores { pub struct Paginate { scores: Vec, guild_id: Option, - mode: Mode, } #[async_trait] @@ -107,9 +98,16 @@ mod scores { let score = &self.scores[page]; let hourglass = msg.react(ctx, '⌛').await?; - let mode = self.mode; - let beatmap = env.beatmaps.get_beatmap(score.beatmap_id, mode).await?; + let beatmap = env + .beatmaps + .get_beatmap(score.beatmap_id, score.mode) + .await?; let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?; + let mode = if beatmap.mode == score.mode { + None + } else { + Some(score.mode) + }; let bm = BeatmapWithMode(beatmap, mode); let user = env .client @@ -153,12 +151,11 @@ mod scores { use youmubot_prelude::*; use crate::discord::oppai_cache::Stats; - use crate::discord::{Beatmap, BeatmapInfo, OsuEnv}; - use crate::models::{Mode, Score}; + use crate::discord::{time_before_now, Beatmap, BeatmapInfo, OsuEnv}; + use crate::models::Score; pub async fn display_scores_table( scores: Vec, - mode: Mode, ctx: &Context, mut on: Message, ) -> Result<()> { @@ -172,7 +169,6 @@ mod scores { Paginate { header: on.content.clone(), scores, - mode, }, ctx, on, @@ -185,7 +181,6 @@ mod scores { pub struct Paginate { header: String, scores: Vec, - mode: Mode, } impl Paginate { @@ -212,14 +207,13 @@ mod scores { let hourglass = msg.react(ctx, '⌛').await?; let plays = &self.scores[start..end]; - let mode = self.mode; let beatmaps = plays .iter() .map(|play| async move { - let beatmap = meta_cache.get_beatmap(play.beatmap_id, mode).await?; + let beatmap = meta_cache.get_beatmap(play.beatmap_id, play.mode).await?; let info = { let b = oppai.get_beatmap(beatmap.beatmap_id).await?; - b.get_info_with(mode, &play.mods) + b.get_info_with(play.mode, &play.mods) }; Ok((beatmap, info)) as Result<(Beatmap, BeatmapInfo)> }) @@ -235,7 +229,7 @@ mod scores { None => { let b = oppai.get_beatmap(p.beatmap_id).await?; let pp = b.get_pp_from( - mode, + p.mode, Some(p.max_combo), Stats::Raw(&p.statistics), &p.mods, @@ -289,15 +283,16 @@ mod scores { beatmap.artist, beatmap.title, beatmap.difficulty_name, - beatmap.short_link(Some(self.mode), &play.mods), + beatmap.short_link(Some(play.mode), &play.mods), ) }) .unwrap_or_else(|| "FETCH_FAILED".to_owned()) }) .collect::>(); - const SCORE_HEADERS: [&str; 6] = ["#", "PP", "Acc", "Ranks", "Mods", "Beatmap"]; - const SCORE_ALIGNS: [Align; 6] = [Right, Right, Right, Right, Right, Left]; + const SCORE_HEADERS: [&str; 7] = + ["#", "PP", "Acc", "Ranks", "Mods", "When", "Beatmap"]; + const SCORE_ALIGNS: [Align; 7] = [Right, Right, Right, Right, Right, Right, Left]; let score_arr = plays .iter() @@ -308,9 +303,10 @@ mod scores { [ format!("{}", id + start + 1), pp.to_string(), - format!("{:.2}%", play.accuracy(self.mode)), + format!("{:.2}%", play.accuracy(play.mode)), format!("{}", rank), play.mods.to_string(), + time_before_now(&play.date), beatmap.clone(), ] }) @@ -486,7 +482,7 @@ mod beatmapset { save_beatmap( &env, msg.channel_id, - &BeatmapWithMode(map.clone(), self.mode.unwrap_or(map.mode)), + &BeatmapWithMode(map.clone(), self.mode), ) .await .pls_ok(); diff --git a/youmubot-osu/src/discord/embeds.rs b/youmubot-osu/src/discord/embeds.rs index b31e345..bc0c4cd 100644 --- a/youmubot-osu/src/discord/embeds.rs +++ b/youmubot-osu/src/discord/embeds.rs @@ -506,7 +506,9 @@ impl<'a> FakeScore<'a> { pub fn embed(self, ctx: &Context) -> Result { let BeatmapWithMode(b, mode) = self.bm; - let info = self.content.get_info_with(*mode, &self.mods); + let info = self + .content + .get_info_with(mode.unwrap_or(b.mode), &self.mods); let attrs = match &info.attrs { rosu_pp::any::PerformanceAttributes::Osu(osu_performance_attributes) => { osu_performance_attributes @@ -540,7 +542,7 @@ impl<'a> FakeScore<'a> { "".into() } else { let pp = self.content.get_pp_from( - *mode, + mode.unwrap_or(b.mode), None, Stats::AccOnly { acc: accuracy, @@ -594,7 +596,7 @@ impl<'a> FakeScore<'a> { "Map stats", b.difficulty .apply_mods(&self.mods, attrs.stars()) - .format_info(*mode, &self.mods, b), + .format_info(mode.unwrap_or(b.mode), &self.mods, b), false, ) .footer(CreateEmbedFooter::new( @@ -707,7 +709,7 @@ pub(crate) fn user_embed(u: User, ex: UserExtras) -> CreateEmbed { "> {}", map.difficulty .apply_mods(&v.mods, info.attrs.stars()) - .format_info(mode, &v.mods, &map) + .format_info(mode.unwrap_or(map.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 4ec9aab..2d08043 100644 --- a/youmubot-osu/src/discord/hook.rs +++ b/youmubot-osu/src/discord/hook.rs @@ -59,7 +59,7 @@ pub fn score_hook<'a>( let mode = score.mode; let content = env.oppai.get_beatmap(score.beatmap_id).await?; let header = env.client.user_header(score.user_id).await?.unwrap(); - Ok((score, BeatmapWithMode(bm, mode), content, header)) + Ok((score, BeatmapWithMode(bm, Some(mode)), content, header)) }) .collect::>() .collect::>() @@ -253,11 +253,10 @@ pub fn hook<'a>( to_join .then(|l| async move { match l.embed { - EmbedType::Beatmap(b, info, mods) => { - handle_beatmap(ctx, &b, info, l.link, l.mode, mods, msg) + EmbedType::Beatmap(b, mode, info, mods) => { + handle_beatmap(ctx, &b, info, l.link, mode, mods, msg) .await .pls_ok(); - let mode = l.mode.unwrap_or(b.mode); let bm = super::BeatmapWithMode(*b, mode); let env = ctx.data.read().await.get::().unwrap().clone(); @@ -266,10 +265,8 @@ pub fn hook<'a>( .await .pls_ok(); } - EmbedType::Beatmapset(b) => { - handle_beatmapset(ctx, b, l.link, l.mode, msg) - .await - .pls_ok(); + EmbedType::Beatmapset(b, mode) => { + handle_beatmapset(ctx, b, l.link, mode, msg).await.pls_ok(); } } }) @@ -289,7 +286,6 @@ async fn handle_beatmap<'a, 'b>( mods: Mods, reply_to: &Message, ) -> Result<()> { - let mode = mode.unwrap_or(beatmap.mode); reply_to .channel_id .send_message( @@ -299,10 +295,21 @@ async fn handle_beatmap<'a, 'b>( MessageBuilder::new() .push("Beatmap information for ") .push_mono_safe(link) + .push(" (") + .push(beatmap.mention(mode, &mods)) + .push(")") .build(), ) - .embed(beatmap_embed(beatmap, mode, &mods, &info)) - .components(vec![beatmap_components(mode, reply_to.guild_id)]) + .embed(beatmap_embed( + beatmap, + mode.unwrap_or(beatmap.mode), + &mods, + &info, + )) + .components(vec![beatmap_components( + mode.unwrap_or(beatmap.mode), + reply_to.guild_id, + )]) .reference_message(reply_to), ) .await?; @@ -317,7 +324,14 @@ async fn handle_beatmapset<'a, 'b>( reply_to: &Message, ) -> Result<()> { let reply = reply_to - .reply(ctx, format!("Beatmapset information for `{}`", link)) + .reply( + ctx, + format!( + "Beatmapset information for `{}` ({})", + link, + beatmaps[0].beatmapset_mention() + ), + ) .await?; crate::discord::display::display_beatmapset( ctx.clone(), diff --git a/youmubot-osu/src/discord/interaction.rs b/youmubot-osu/src/discord/interaction.rs index 9e442d1..2f11565 100644 --- a/youmubot-osu/src/discord/interaction.rs +++ b/youmubot-osu/src/discord/interaction.rs @@ -14,8 +14,9 @@ use crate::{discord::embeds::FakeScore, mods::UnparsedMods, Mode, Mods, UserHead use super::{ display::ScoreListStyle, embeds::beatmap_embed, - server_rank::{display_rankings_table, get_leaderboard, OrderBy}, - BeatmapWithMode, OsuEnv, + link_parser::EmbedType, + server_rank::{display_rankings_table, get_leaderboard_from_embed, OrderBy}, + BeatmapWithMode, LoadRequest, OsuEnv, }; pub(super) const BTN_CHECK: &str = "youmubot_osu_btn_check"; @@ -67,7 +68,7 @@ pub fn handle_check_button<'a>( let msg = &*comp.message; let env = ctx.data.read().await.get::().unwrap().clone(); - let (bm, _) = super::load_beatmap(&env, comp.channel_id, Some(msg)) + let embed = super::load_beatmap(&env, comp.channel_id, Some(msg), LoadRequest::Any) .await .unwrap(); let user = match env.saved_users.by_user_id(comp.user.id).await? { @@ -79,15 +80,15 @@ pub fn handle_check_button<'a>( }; let header = UserHeader::from(user.clone()); - let scores = super::do_check(&env, &vec![bm.clone()], None, &header).await?; + let scores = super::do_check(&env, &embed, None, &header).await?; if scores.is_empty() { comp.create_followup( &ctx, CreateInteractionResponseFollowup::new().content(format!( - "No plays found for [`{}`]() on `{}`.", + "No plays found for [`{}`]() on {}.", user.username, user.id, - bm.short_link(Mods::NOMOD) + embed.mention(), )), ) .await?; @@ -98,10 +99,10 @@ pub fn handle_check_button<'a>( .create_followup( &ctx, CreateInteractionResponseFollowup::new().content(format!( - "Here are the scores by [`{}`]() on `{}`!", + "Here are the scores by [`{}`]() on {}!", user.username, user.id, - bm.short_link(Mods::NOMOD) + embed.mention() )), ) .await?; @@ -110,7 +111,7 @@ pub fn handle_check_button<'a>( let guild_id = comp.guild_id; spawn_future(async move { ScoreListStyle::Grid - .display_scores(scores, bm.1, &ctx, guild_id, reply) + .display_scores(scores, &ctx, guild_id, reply) .await .pls_ok(); }); @@ -189,11 +190,16 @@ pub fn handle_simulate_button<'a>( let env = ctx.data.read().await.get::().unwrap().clone(); - let (bm, _) = super::load_beatmap(&env, comp.channel_id, Some(msg)) + let embed = super::load_beatmap(&env, comp.channel_id, Some(msg), LoadRequest::Beatmap) .await .unwrap(); - let b = &bm.0; - let mode = bm.1; + let (b, mode) = match embed { + EmbedType::Beatmap(beatmap, mode, _, _) => { + let mode = mode.unwrap_or(beatmap.mode); + (beatmap, mode) + } + EmbedType::Beatmapset(_, _) => return Err(Error::msg("Cannot find any beatmap")), + }; let content = env.oppai.get_beatmap(b.beatmap_id).await?; let info = content.get_info_with(mode, Mods::NOMOD); @@ -227,7 +233,9 @@ pub fn handle_simulate_button<'a>( query.interaction.defer(&ctx).await?; - if let Err(err) = handle_simluate_query(ctx, &env, &query, bm).await { + if let Err(err) = + handle_simluate_query(ctx, &env, &query, BeatmapWithMode(*b, Some(mode))).await + { query .interaction .create_followup( @@ -251,7 +259,7 @@ async fn handle_simluate_query( bm: BeatmapWithMode, ) -> Result<()> { let b = &bm.0; - let mode = bm.1; + let mode = bm.1.unwrap_or(b.mode); let content = env.oppai.get_beatmap(b.beatmap_id).await?; let score: FakeScore = { @@ -305,54 +313,58 @@ async fn handle_last_req( let env = ctx.data.read().await.get::().unwrap().clone(); - let (bm, mods_def) = super::load_beatmap(&env, comp.channel_id, Some(msg)) - .await - .unwrap(); - let BeatmapWithMode(b, m) = &bm; + let embed = super::load_beatmap( + &env, + comp.channel_id, + Some(msg), + if is_beatmapset_req { + LoadRequest::Beatmapset + } else { + LoadRequest::Any + }, + ) + .await + .unwrap(); - let mods = mods_def.unwrap_or_default(); - - if is_beatmapset_req { - let beatmapset = env - .beatmaps - .get_beatmapset(bm.0.beatmapset_id, None) - .await?; - let reply = comp - .create_followup( - &ctx, - CreateInteractionResponseFollowup::new() - .content(format!("Beatmapset `{}`", bm.0.beatmapset_mention())), + match embed { + EmbedType::Beatmapset(beatmapset, mode) => { + let reply = comp + .create_followup( + &ctx, + CreateInteractionResponseFollowup::new().content(format!( + "Beatmapset `{}`", + beatmapset[0].beatmapset_mention() + )), + ) + .await?; + super::display::display_beatmapset( + ctx.clone(), + beatmapset, + mode, + None, + comp.guild_id, + reply, ) .await?; - super::display::display_beatmapset( - ctx.clone(), - beatmapset, - None, - None, - comp.guild_id, - reply, - ) - .await?; - return Ok(()); - } else { - let info = env - .oppai - .get_beatmap(b.beatmap_id) - .await? - .get_possible_pp_with(*m, &mods); - comp.create_followup( - &ctx, - serenity::all::CreateInteractionResponseFollowup::new() - .content(format!( - "Information for beatmap `{}`", - bm.short_link(&mods) - )) - .embed(beatmap_embed(b, *m, &mods, &info)) - .components(vec![beatmap_components(bm.1, comp.guild_id)]), - ) - .await?; - // Save the beatmap... - super::cache::save_beatmap(&env, msg.channel_id, &bm).await?; + return Ok(()); + } + EmbedType::Beatmap(b, m, _, mods) => { + let info = env + .oppai + .get_beatmap(b.beatmap_id) + .await? + .get_possible_pp_with(m.unwrap_or(b.mode), &mods); + comp.create_followup( + &ctx, + serenity::all::CreateInteractionResponseFollowup::new() + .content(format!("Information for beatmap {}", b.mention(m, &mods))) + .embed(beatmap_embed(&*b, m.unwrap_or(b.mode), &mods, &info)) + .components(vec![beatmap_components(m.unwrap_or(b.mode), comp.guild_id)]), + ) + .await?; + // Save the beatmap... + super::cache::save_beatmap(&env, msg.channel_id, &BeatmapWithMode(*b, m)).await?; + } } Ok(()) @@ -380,20 +392,23 @@ pub fn handle_lb_button<'a>( let env = ctx.data.read().await.get::().unwrap().clone(); - let (bm, _) = super::load_beatmap(&env, comp.channel_id, Some(msg)) + let embed = super::load_beatmap(&env, comp.channel_id, Some(msg), LoadRequest::Any) .await .unwrap(); let order = OrderBy::default(); let guild = comp.guild_id.expect("Guild-only command"); - let scores = get_leaderboard(ctx, &env, &bm, false, order, guild).await?; + let scoreboard_msg = embed.mention(); + let (scores, show_diff) = + get_leaderboard_from_embed(ctx, &env, embed, None, false, order, guild).await?; if scores.is_empty() { comp.create_followup( &ctx, - CreateInteractionResponseFollowup::new().content( - "No scores have been recorded for this beatmap from anyone in this server.", - ), + CreateInteractionResponseFollowup::new().content(format!( + "No scores have been recorded for {} from anyone in this server.", + scoreboard_msg + )), ) .await?; return Ok(()); @@ -402,13 +417,11 @@ pub fn handle_lb_button<'a>( let reply = comp .create_followup( &ctx, - CreateInteractionResponseFollowup::new().content(format!( - "Here are the top scores on beatmap `{}`!", - bm.short_link(Mods::NOMOD) - )), + CreateInteractionResponseFollowup::new() + .content(format!("Here are the top scores on {}!", scoreboard_msg)), ) .await?; - display_rankings_table(ctx, reply, scores, &bm, order).await?; + display_rankings_table(ctx, reply, scores, show_diff, order).await?; Ok(()) }) } diff --git a/youmubot-osu/src/discord/link_parser.rs b/youmubot-osu/src/discord/link_parser.rs index bb078c5..50cdc7b 100644 --- a/youmubot-osu/src/discord/link_parser.rs +++ b/youmubot-osu/src/discord/link_parser.rs @@ -9,15 +9,24 @@ use youmubot_prelude::*; use super::{oppai_cache::BeatmapInfoWithPP, OsuEnv}; +#[derive(Debug, Clone)] pub enum EmbedType { - Beatmap(Box, BeatmapInfoWithPP, Mods), - Beatmapset(Vec), + Beatmap(Box, Option, BeatmapInfoWithPP, Mods), + Beatmapset(Vec, Option), +} + +impl EmbedType { + pub fn mention(&self) -> String { + match self { + EmbedType::Beatmap(beatmap, mode, _, mods) => beatmap.mention(*mode, mods), + EmbedType::Beatmapset(vec, _) => vec[0].beatmapset_mention(), + } + } } pub struct ToPrint<'a> { pub embed: EmbedType, pub link: &'a str, - pub mode: Option, } lazy_static! { @@ -66,7 +75,6 @@ pub fn parse_old_links<'a>( Ok(ToPrint { embed, link: capture.get(0).unwrap().as_str(), - mode, }) }) .collect::>() @@ -104,7 +112,7 @@ pub fn parse_new_links<'a>( .await } }?; - Ok(ToPrint { embed, link, mode }) + Ok(ToPrint { embed, link }) }) .collect::>() .filter_map(|v: Result| future::ready(v.pls_ok())) @@ -133,14 +141,14 @@ pub fn parse_short_links<'a>( "s" => EmbedType::from_beatmapset_id(env, id, mode).await?, _ => unreachable!(), }; - Ok(ToPrint { embed, link, mode }) + Ok(ToPrint { embed, link }) }) .collect::>() .filter_map(|v: Result| future::ready(v.pls_ok())) } impl EmbedType { - async fn from_beatmap_id( + pub(crate) async fn from_beatmap_id( env: &OsuEnv, beatmap_id: u64, mode: Option, @@ -158,16 +166,17 @@ impl EmbedType { .await .map(|b| b.get_possible_pp_with(mode, &mods))? }; - Ok(Self::Beatmap(Box::new(bm), info, mods)) + Ok(Self::Beatmap(Box::new(bm), mode, info, mods)) } - async fn from_beatmapset_id( + pub(crate) async fn from_beatmapset_id( env: &OsuEnv, beatmapset_id: u64, mode: Option, ) -> Result { Ok(Self::Beatmapset( env.beatmaps.get_beatmapset(beatmapset_id, mode).await?, + mode, )) } } diff --git a/youmubot-osu/src/discord/mod.rs b/youmubot-osu/src/discord/mod.rs index dc83384..50c98a4 100644 --- a/youmubot-osu/src/discord/mod.rs +++ b/youmubot-osu/src/discord/mod.rs @@ -2,7 +2,9 @@ use std::{borrow::Borrow, collections::HashMap as Map, str::FromStr, sync::Arc}; use chrono::Utc; use futures_util::join; + use interaction::{beatmap_components, score_components}; +use link_parser::EmbedType; use oppai_cache::BeatmapInfoWithPP; use rand::seq::IteratorRandom; use serenity::{ @@ -224,15 +226,11 @@ pub async fn mania(ctx: &Context, msg: &Message, args: Args) -> CommandResult { } #[derive(Debug, Clone)] -pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode); +pub(crate) struct BeatmapWithMode(pub Beatmap, pub Option); impl BeatmapWithMode { - pub fn short_link(&self, mods: &Mods) -> String { - self.0.short_link(Some(self.1), mods) - } - fn mode(&self) -> Mode { - self.1 + self.1.unwrap_or(self.0.mode) } } @@ -339,7 +337,7 @@ pub(crate) async fn handle_save_respond( let osu_client = &env.client; async fn check(client: &OsuHttpClient, u: &User, mode: Mode, map_id: u64) -> Result { Ok(client - .user_recent(UserID::ID(u.id), |f| f.mode(Mode::Std).limit(1)) + .user_recent(UserID::ID(u.id), |f| f.mode(mode).limit(1)) .await? .into_iter() .take(1) @@ -518,7 +516,7 @@ impl UserExtras { .get_beatmap(s.beatmap_id) .await? .get_info_with(mode, &s.mods); - Some((s, BeatmapWithMode(beatmap, mode), info)) + Some((s, BeatmapWithMode(beatmap, Some(mode)), info)) } else { None }; @@ -691,7 +689,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu ) .await?; style - .display_scores(plays, mode, ctx, reply.guild_id, reply) + .display_scores(plays, ctx, reply.guild_id, reply) .await?; } Nth::Nth(nth) => { @@ -705,7 +703,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu .count(); let beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?; let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?; - let beatmap_mode = BeatmapWithMode(beatmap, mode); + let beatmap_mode = BeatmapWithMode(beatmap, Some(mode)); msg.channel_id .send_message( @@ -764,7 +762,7 @@ pub async fn pins(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult ) .await?; style - .display_scores(plays, mode, ctx, reply.guild_id, reply) + .display_scores(plays, ctx, reply.guild_id, reply) .await?; } Nth::Nth(nth) => { @@ -773,7 +771,7 @@ pub async fn pins(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult }; let beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?; let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?; - let beatmap_mode = BeatmapWithMode(beatmap, mode); + let beatmap_mode = BeatmapWithMode(beatmap, Some(mode)); msg.channel_id .send_message( @@ -807,46 +805,107 @@ impl FromStr for OptBeatmapSet { } } +pub(crate) async fn load_beatmap_from_channel( + env: &OsuEnv, + channel_id: serenity::all::ChannelId, +) -> Option { + let BeatmapWithMode(b, m) = cache::get_beatmap(env, channel_id).await.ok().flatten()?; + let mods = Mods::NOMOD.clone(); + let info = env + .oppai + .get_beatmap(b.beatmap_id) + .await + .pls_ok()? + .get_possible_pp_with(m.unwrap_or(b.mode), &mods); + Some(EmbedType::Beatmap( + Box::new(b), + m, + info, + Mods::NOMOD.clone(), + )) +} + +#[derive(PartialEq, Eq, Clone, Copy, Default)] +pub(crate) enum LoadRequest { + #[default] + Any, + Beatmap, + Beatmapset, +} + /// Load the mentioned beatmap from the given message. pub(crate) async fn load_beatmap( env: &OsuEnv, channel_id: serenity::all::ChannelId, referenced: Option<&impl Borrow>, -) -> Option<(BeatmapWithMode, Option)> { - use link_parser::{parse_short_links, EmbedType}; - if let Some(replied) = referenced { + req: LoadRequest, +) -> Option { + /* If the request is Beatmapset, we keep a fallback match on beatmap, and later convert it to a beatmapset. */ + let mut fallback: Option = None; + async fn collect_referenced( + env: &OsuEnv, + fallback: &mut Option, + req: LoadRequest, + replied: &impl Borrow, + ) -> Option { + use link_parser::*; async fn try_content( env: &OsuEnv, + req: LoadRequest, + fallback: &mut Option, content: &str, - ) -> Option<(BeatmapWithMode, Option)> { - let tp = parse_short_links(env, content).next().await?; - match tp.embed { - EmbedType::Beatmap(b, _, mods) => { - let mode = tp.mode.unwrap_or(b.mode); - Some((BeatmapWithMode(*b, mode), Some(mods))) - } - _ => None, - } + ) -> Option { + parse_short_links(env, content) + .filter(|e| { + future::ready(match &e.embed { + EmbedType::Beatmap(_, _, _, _) => { + if fallback.is_none() { + fallback.replace(e.embed.clone()); + } + req == LoadRequest::Beatmap || req == LoadRequest::Any + } + EmbedType::Beatmapset(_, _) => { + req == LoadRequest::Beatmapset || req == LoadRequest::Any + } + }) + }) + .next() + .await + .map(|v| v.embed) + } + if let Some(v) = try_content(env, req, fallback, &replied.borrow().content).await { + return Some(v); } for embed in &replied.borrow().embeds { for field in &embed.fields { - if let Some(v) = try_content(env, &field.value).await { + if let Some(v) = try_content(env, req, fallback, &field.value).await { return Some(v); } } if let Some(desc) = &embed.description { - if let Some(v) = try_content(env, desc).await { + if let Some(v) = try_content(env, req, fallback, desc).await { return Some(v); } } } - if let Some(v) = try_content(env, &replied.borrow().content).await { - return Some(v); - } + None } - let b = cache::get_beatmap(env, channel_id).await.ok().flatten(); - b.map(|b| (b, None)) + let embed = match referenced { + Some(r) => collect_referenced(env, &mut fallback, req, r).await, + None => load_beatmap_from_channel(env, channel_id).await, + }; + + if req == LoadRequest::Beatmapset { + if embed.is_none() { + if let Some(EmbedType::Beatmap(b, mode, _, _)) = fallback { + return EmbedType::from_beatmapset_id(env, b.beatmapset_id, mode) + .await + .ok(); + } + } + } + embed } #[command] @@ -858,58 +917,56 @@ pub(crate) async fn load_beatmap( pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let env = ctx.data.read().await.get::().unwrap().clone(); - let b = load_beatmap(&env, msg.channel_id, msg.referenced_message.as_ref()).await; let beatmapset = args.find::().is_ok(); + let Some(embed) = load_beatmap( + &env, + msg.channel_id, + msg.referenced_message.as_ref(), + if beatmapset { + LoadRequest::Beatmapset + } else { + LoadRequest::Any + }, + ) + .await + else { + msg.reply(&ctx, "No beatmap was queried on this channel.") + .await?; + return Ok(()); + }; + let umods = args.find::().ok(); - match b { - Some((bm, mods_def)) => { - let mods = args.find::().ok(); - if beatmapset { - let beatmapset = env - .beatmaps - .get_beatmapset( - bm.0.beatmapset_id, - None, /* Note that we cannot know, so don't force that */ - ) - .await?; - let reply = msg - .reply(&ctx, "Here is the beatmapset you requested!") - .await?; - display::display_beatmapset( - ctx.clone(), - beatmapset, - None, - mods, - msg.guild_id, - reply, - ) - .await?; - return Ok(()); - } - let mods = match mods { - Some(m) => m.to_mods(bm.mode())?, - None => mods_def.unwrap_or_default(), + let content_type = embed.mention(); + match embed { + EmbedType::Beatmap(b, mode_, _, mods) => { + let mode = mode_.unwrap_or(b.mode); + let mods = match umods { + Some(m) => m.to_mods(mode)?, + None => mods, }; let info = env .oppai - .get_beatmap(bm.0.beatmap_id) + .get_beatmap(b.beatmap_id) .await? - .get_possible_pp_with(bm.1, &mods); + .get_possible_pp_with(mode, &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)) - .components(vec![beatmap_components(bm.1, msg.guild_id)]) + .content(format!("Information for {}", content_type)) + .embed(beatmap_embed(&b, mode, &mods, &info)) + .components(vec![beatmap_components(mode, msg.guild_id)]) .reference_message(msg), ) .await?; // Save the beatmap... - cache::save_beatmap(&env, msg.channel_id, &bm).await?; + cache::save_beatmap(&env, msg.channel_id, &BeatmapWithMode(*b, mode_)).await?; } - None => { - msg.reply(&ctx, "No beatmap was queried on this channel.") + EmbedType::Beatmapset(beatmaps, mode) => { + let reply = msg + .reply(&ctx, format!("Information for {}", content_type)) + .await?; + display::display_beatmapset(ctx.clone(), beatmaps, mode, umods, msg.guild_id, reply) .await?; } } @@ -924,26 +981,27 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult #[max_args(3)] pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { let env = ctx.data.read().await.get::().unwrap().clone(); - let bm = load_beatmap(&env, msg.channel_id, msg.referenced_message.as_ref()).await; - - let bm = match bm { - Some((bm, _)) => bm, - None => { - msg.reply(&ctx, "No beatmap queried on this channel.") - .await?; - return Ok(()); - } + let Some(embed) = load_beatmap( + &env, + msg.channel_id, + msg.referenced_message.as_ref(), + LoadRequest::Any, + ) + .await + else { + msg.reply(&ctx, "No beatmap queried on this channel.") + .await?; + return Ok(()); }; - let mode = bm.1; + 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, &vec![bm.clone()], umods, &user).await?; + let scores = do_check(&env, &embed, umods, &user).await?; if scores.is_empty() { msg.reply(&ctx, "No scores found").await?; @@ -955,12 +1013,12 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul format!( "Here are the scores by `{}` on {}!", &user.username, - bm.0.mention(Some(bm.1), &mods) + embed.mention() ), ) .await?; style - .display_scores(scores, mode, ctx, msg.guild_id, reply) + .display_scores(scores, ctx, msg.guild_id, reply) .await?; Ok(()) @@ -968,30 +1026,41 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul pub(crate) async fn do_check( env: &OsuEnv, - bm: &[BeatmapWithMode], + embed: &EmbedType, mods: Option, user: &UserHeader, ) -> Result> { - let osu_client = &env.client; + async fn fetch_for_beatmap( + env: &OsuEnv, + b: &Beatmap, + mode_override: Option, + mods: &Option, + user: &UserHeader, + ) -> Result> { + let osu_client = &env.client; + let m = mode_override.unwrap_or(b.mode); + 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 + }) + .await + } - 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() - .flatten() - .collect::>(); + let mut scores = match embed { + EmbedType::Beatmap(beatmap, mode, _, _) => { + fetch_for_beatmap(env, &**beatmap, *mode, &mods, user).await? + } + EmbedType::Beatmapset(vec, mode) => vec + .iter() + .map(|b| fetch_for_beatmap(env, b, *mode, &mods, user)) + .collect::>() + .try_collect::>() + .await? + .concat(), + }; scores.sort_by(|a, b| { b.pp.unwrap_or(-1.0) .partial_cmp(&a.pp.unwrap_or(-1.0)) @@ -1031,7 +1100,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult let beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?; let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?; - let beatmap = BeatmapWithMode(beatmap, mode); + let beatmap = BeatmapWithMode(beatmap, Some(mode)); msg.channel_id .send_message(&ctx, { @@ -1061,7 +1130,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult ) .await?; style - .display_scores(plays, mode, ctx, msg.guild_id, reply) + .display_scores(plays, ctx, msg.guild_id, reply) .await?; } } @@ -1182,3 +1251,20 @@ pub(in crate::discord) async fn calculate_weighted_map_age( / scales().iter().take(scores.len()).sum::()) .floor() as i64) } + +pub(crate) fn time_before_now(time: &chrono::DateTime) -> String { + let dur = Utc::now() - time; + if dur.num_days() >= 365 { + format!("{}Y", dur.num_days() / 365) + } else if dur.num_days() >= 30 { + format!("{}M", dur.num_days() / 30) + } else if dur.num_days() >= 1 { + format!("{}d", dur.num_days()) + } else if dur.num_hours() >= 1 { + format!("{}h", dur.num_hours()) + } else if dur.num_minutes() >= 1 { + format!("{}m", dur.num_minutes()) + } else { + format!("{}s", dur.num_seconds()) + } +} diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index 7f56b57..ad0b166 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -25,10 +25,13 @@ use youmubot_prelude::{ }; use crate::{ - discord::{db::OsuUser, display::ScoreListStyle, oppai_cache::Stats, BeatmapWithMode}, - models::{Mode, Mods}, + discord::{ + db::OsuUser, display::ScoreListStyle, link_parser::EmbedType, oppai_cache::Stats, + time_before_now, + }, + models::Mode, request::UserID, - Score, + Beatmap, Score, }; use super::{ModeArg, OsuEnv}; @@ -376,20 +379,23 @@ pub async fn show_leaderboard(ctx: &Context, msg: &Message, mut args: Args) -> C let style = args.single::().unwrap_or_default(); let guild = msg.guild_id.expect("Guild-only command"); let env = ctx.data.read().await.get::().unwrap().clone(); - let Some((bm, _)) = - super::load_beatmap(&env, msg.channel_id, msg.referenced_message.as_ref()).await + let Some(beatmap) = super::load_beatmap( + &env, + msg.channel_id, + msg.referenced_message.as_ref(), + crate::discord::LoadRequest::Any, + ) + .await else { msg.reply(&ctx, "No beatmap queried on this channel.") .await?; return Ok(()); }; - - let scores = { - let reaction = msg.react(ctx, '⌛').await?; - let s = get_leaderboard(ctx, &env, &bm, show_all, order, guild).await?; - reaction.delete(&ctx).await?; - s - }; + let reaction = msg.react(ctx, '⌛').await?; + let scoreboard_msg = beatmap.mention(); + let (scores, show_diff) = + get_leaderboard_from_embed(ctx, &env, beatmap, None, show_all, order, guild).await?; + reaction.delete(&ctx).await?; if scores.is_empty() { msg.reply(&ctx, "No scores have been recorded for this beatmap.") @@ -402,28 +408,24 @@ pub async fn show_leaderboard(ctx: &Context, msg: &Message, mut args: Args) -> C let reply = msg .reply( &ctx, - format!( - "⌛ Loading top scores on beatmap `{}`...", - bm.short_link(Mods::NOMOD) - ), + format!("⌛ Loading top scores on {}...", scoreboard_msg), ) .await?; - display_rankings_table(ctx, reply, scores, &bm, order).await?; + display_rankings_table(ctx, reply, scores, show_diff, order).await?; } ScoreListStyle::Grid => { let reply = msg .reply( &ctx, format!( - "Here are the top scores on beatmap `{}` of this server!", - bm.short_link(Mods::NOMOD) + "Here are the top scores on {} of this server!", + scoreboard_msg ), ) .await?; style .display_scores( scores.into_iter().map(|s| s.score).collect(), - bm.1, ctx, Some(guild), reply, @@ -439,19 +441,30 @@ pub struct Ranking { pub pp: f64, // calculated pp or score pp pub official: bool, // official = pp is from bancho pub member: Arc, + pub beatmap: Arc, pub score: Score, + pub star: f64, } -pub async fn get_leaderboard( +async fn get_leaderboard( ctx: &Context, env: &OsuEnv, - bm: &BeatmapWithMode, + beatmaps: impl IntoIterator, + mode_override: Option, show_unranked: bool, order: OrderBy, guild: GuildId, ) -> Result> { - let BeatmapWithMode(beatmap, mode) = bm; - let oppai_map = env.oppai.get_beatmap(beatmap.beatmap_id).await?; + let oppai_maps = beatmaps + .into_iter() + .map(|b| async move { + let op = env.oppai.get_beatmap(b.beatmap_id).await?; + let r: Result<_> = Ok((Arc::new(b), op)); + r + }) + .collect::>() + .try_collect::>() + .await?; let osu_users = env .saved_users .all() @@ -465,39 +478,48 @@ pub async fn get_leaderboard( .query_members(&ctx, guild) .await? .iter() - .filter_map(|m| osu_users.get(&m.user.id).map(|ou| (m.distinct(), ou.id))) - .map(|(mem, osu_id)| { - env.client - .scores(bm.0.beatmap_id, move |f| { - f.user(UserID::ID(osu_id)).mode(bm.1) - }) - .map(|r| Some((mem, r.ok()?))) + .filter_map(|m| { + osu_users + .get(&m.user.id) + .map(|ou| (Arc::new(m.distinct()), ou.id)) + }) + .flat_map(|(mem, osu_id)| { + oppai_maps.iter().map(move |(b, op)| { + let mem = mem.clone(); + env.client + .scores(b.beatmap_id, move |f| { + f.user(UserID::ID(osu_id)).mode(mode_override) + }) + .map(move |r| Some((b, op, mem.clone(), r.ok()?))) + }) }) .collect::>() .filter_map(future::ready) .collect::>() .await .into_iter() - .flat_map(|(mem, scores)| { - let mem = Arc::new(mem); + .flat_map(|(b, op, mem, scores)| { scores .into_iter() .map(|score| { let pp = score.pp.map(|v| (true, v)).unwrap_or_else(|| { ( false, - oppai_map.get_pp_from( - *mode, + op.get_pp_from( + mode_override.unwrap_or(b.mode), Some(score.max_combo), Stats::Raw(&score.statistics), &score.mods, ), ) }); + let info = op.get_info_with(score.mode, &score.mods); Ranking { pp: pp.1, official: pp.0, + beatmap: b.clone(), member: mem.clone(), + star: info.attrs.stars(), score, } }) @@ -536,11 +558,55 @@ pub async fn get_leaderboard( Ok(scores) } +pub async fn get_leaderboard_from_embed( + ctx: &Context, + env: &OsuEnv, + embed: EmbedType, + mode_override: Option, + show_unranked: bool, + order: OrderBy, + guild: GuildId, +) -> Result<(Vec, bool /* should show diff */)> { + Ok(match embed { + EmbedType::Beatmap(map, mode, _, _) => { + let iter = std::iter::once(*map); + let scores = get_leaderboard( + ctx, + &env, + iter, + mode_override.or(mode), + show_unranked, + order, + guild, + ) + .await?; + (scores, false) + } + EmbedType::Beatmapset(maps, _) if maps.is_empty() => (vec![], false), + EmbedType::Beatmapset(maps, mode) => { + let show_diff = maps.len() > 1; + ( + get_leaderboard( + ctx, + &env, + maps, + mode_override.or(mode), + show_unranked, + order, + guild, + ) + .await?, + show_diff, + ) + } + }) +} + pub async fn display_rankings_table( ctx: &Context, to: Message, scores: Vec, - bm: &BeatmapWithMode, + show_diff: bool, order: OrderBy, ) -> Result<()> { let has_lazer_score = scores.iter().any(|v| v.score.mods.is_lazer); @@ -558,14 +624,33 @@ pub async fn display_rankings_table( return Box::pin(future::ready(Ok(false))); } let scores = scores[start..end].to_vec(); - let bm = (bm.0.clone(), bm.1); let header = header.clone(); Box::pin(async move { - const SCORE_HEADERS: [&str; 8] = - ["#", "Score", "Mods", "Rank", "Acc", "Combo", "Miss", "User"]; - const PP_HEADERS: [&str; 8] = - ["#", "PP", "Mods", "Rank", "Acc", "Combo", "Miss", "User"]; - const ALIGNS: [Align; 8] = [Right, Right, Right, Right, Right, Right, Right, Left]; + let headers: [&'static str; 9] = [ + "#", + match order { + OrderBy::PP => "pp", + OrderBy::Score => "Score", + }, + if show_diff { "Map" } else { "Mods" }, + "Rank", + "Acc", + "Combo", + "Miss", + "When", + "User", + ]; + let aligns: [Align; 9] = [ + Right, + Right, + if show_diff { Left } else { Right }, + Right, + Right, + Right, + Right, + Right, + Left, + ]; let score_arr = scores .iter() @@ -575,9 +660,11 @@ pub async fn display_rankings_table( id, Ranking { pp, + beatmap, official, member, score, + star, }, )| { [ @@ -594,21 +681,35 @@ pub async fn display_rankings_table( }) } }, - score.mods.to_string(), + if show_diff { + let trimmed_diff = if beatmap.difficulty_name.len() > 20 { + let mut s = beatmap.difficulty_name.clone(); + s.truncate(17); + s + "..." + } else { + beatmap.difficulty_name.clone() + }; + format!( + "[{:.2}*] {} {}", + star, + trimmed_diff, + score.mods.to_string() + ) + } else { + score.mods.to_string() + }, score.rank.to_string(), - format!("{:.2}%", score.accuracy(bm.1)), + format!("{:.2}%", score.accuracy(score.mode)), format!("{}x", score.max_combo), format!("{}", score.count_miss), + time_before_now(&score.date), member.to_string(), ] }, ) .collect::>(); - let score_table = match order { - OrderBy::PP => table_formatting(&PP_HEADERS, &ALIGNS, score_arr), - OrderBy::Score => table_formatting(&SCORE_HEADERS, &ALIGNS, score_arr), - }; + let score_table = table_formatting(&headers, &aligns, score_arr); let content = MessageBuilder::new() .push_line(header.as_ref()) .push_line(score_table) @@ -617,15 +718,6 @@ pub async fn display_rankings_table( page + 1, total_pages, )) - .push( - if let crate::models::ApprovalStatus::Ranked(_) = bm.0.approval { - "" - } else if order == OrderBy::PP { - "PP was calculated by `oppai-rs`, **not** official values.\n" - } else { - "" - }, - ) .build(); m.edit(&ctx, EditMessage::new().content(content)).await?; diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index 753df0b..27bd6cc 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -452,7 +452,7 @@ impl Beatmap { pub fn mention(&self, override_mode: Option, mods: &Mods) -> String { format!( - "[`{}`]({})", + "[`{}`](<{}>)", self.short_link(override_mode, mods), self.mode_link(override_mode), )