Add check command

This commit is contained in:
Natsu Kagami 2024-12-31 06:43:44 +01:00 committed by Natsu Kagami
parent 0db52c5c2b
commit 3f9db46032
4 changed files with 190 additions and 34 deletions

View file

@ -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<U: HasOsuEnv>(_ctx: CmdContext<'_, U>) -> Result<()> {
Ok(())
@ -308,20 +320,7 @@ async fn beatmap<U: HasOsuEnv>(
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<U: HasOsuEnv>(
)]),
)
.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<U: HasOsuEnv>(
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<U: HasOsuEnv>(
ctx: CmdContext<'_, U>,
#[description = "A link or shortlink to the beatmap or beatmapset"] map: Option<String>,
#[description = "osu! username"] username: Option<String>,
#[description = "Discord username"] discord_name: Option<User>,
#[description = "Sort scores by"] sort: Option<SortScoreBy>,
#[description = "Reverse the sorting order"] reverse: Option<bool>,
#[description = "Filter the mods on the scores"] mods: Option<UnparsedMods>,
#[description = "Filter the mode of the scores"] mode: Option<Mode>,
#[description = "Find all scores in the beatmapset instead"] beatmapset: Option<bool>,
#[description = "Score listing style"] style: Option<ScoreListStyle>,
) -> 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<String>,
discord_name: Option<User>,
@ -405,8 +538,9 @@ async fn parse_map_input(
env: &OsuEnv,
input: Option<String>,
mode: Option<Mode>,
beatmapset: Option<bool>,
) -> Result<EmbedType> {
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)
}

View file

@ -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 {

View file

@ -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,

View file

@ -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::<UnparsedMods>()
.ok()
.unwrap_or_default()
.to_mods(mode)?;
let umods = args.find::<UnparsedMods>().ok();
let mods = umods.clone().unwrap_or_default().to_mods(mode)?;
let style = args
.single::<ScoreListStyle>()
.unwrap_or(ScoreListStyle::Grid);
let username_arg = args.single::<UsernameArg>().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<UnparsedMods>,
user: &UserHeader,
) -> Result<Vec<Score>> {
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::<FuturesUnordered<_>>()
.try_collect::<Vec<_>>()
.await?
.into_iter()
.filter(|s| s.mods.contains(mods))
.flatten()
.collect::<Vec<_>>();
scores.sort_by(|a, b| {
b.pp.unwrap_or(-1.0)