osu: [Big!] rework load_beatmap so that it can retrieve beatmapsets

Also
- scoreboard can handle beatmapsets now
- "when" column is included
- beatmapWithMode's mode field is now optional
This commit is contained in:
Natsu Kagami 2025-02-20 18:07:56 +01:00 committed by Natsu Kagami
parent 60a72dad85
commit b302bd3ce1
13 changed files with 564 additions and 378 deletions

View file

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

View file

@ -3,7 +3,7 @@ use crate::models::*;
pub struct LastBeatmap {
pub channel_id: i64,
pub beatmap: Vec<u8>,
pub mode: u8,
pub mode: Option<u8>,
}
impl LastBeatmap {

View file

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

View file

@ -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<U: HasOsuEnv>(
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<U: HasOsuEnv>(
.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<U: HasOsuEnv>(
// 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<U: HasOsuEnv>(
)
.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<U: HasOsuEnv>(
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<U: HasOsuEnv>(
});
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<U: HasOsuEnv>(
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<U: HasOsuEnv>(
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<U: HasOsuEnv>(
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<U: HasOsuEnv>(
ctx.serenity_context(),
reply,
scores,
&bm,
show_diff,
sort.unwrap_or_default(),
)
.await?;
@ -610,7 +580,6 @@ async fn leaderboard<U: HasOsuEnv>(
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<EmbedType> {
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::<u64>() {
@ -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

View file

@ -81,11 +81,17 @@ impl OsuLastBeatmap {
}
impl OsuLastBeatmap {
pub async fn by_channel(&self, id: impl Into<ChannelId>) -> Result<Option<(Beatmap, Mode)>> {
pub async fn by_channel(
&self,
id: impl Into<ChannelId>,
) -> Result<Option<(Beatmap, Option<Mode>)>> {
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<ChannelId>,
beatmap: &Beatmap,
mode: Mode,
mode: Option<Mode>,
) -> 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(())

View file

@ -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<Score>,
mode: Mode,
ctx: &Context,
guild_id: Option<GuildId>,
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<Score>,
mode: Mode,
ctx: &Context,
guild_id: Option<GuildId>,
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<Score>,
guild_id: Option<GuildId>,
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<Score>,
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<Score>,
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::<Vec<_>>();
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();

View file

@ -506,7 +506,9 @@ impl<'a> FakeScore<'a> {
pub fn embed(self, ctx: &Context) -> Result<CreateEmbed> {
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(),

View file

@ -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::<FuturesOrdered<_>>()
.collect::<Vec<_>>()
@ -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::<OsuEnv>().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(),

View file

@ -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::<OsuEnv>().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 [`{}`](<https://osu.ppy.sh/users/{}>) on `{}`.",
"No plays found for [`{}`](<https://osu.ppy.sh/users/{}>) 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 [`{}`](<https://osu.ppy.sh/users/{}>) on `{}`!",
"Here are the scores by [`{}`](<https://osu.ppy.sh/users/{}>) 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::<OsuEnv>().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::<OsuEnv>().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::<OsuEnv>().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(())
})
}

View file

@ -9,15 +9,24 @@ use youmubot_prelude::*;
use super::{oppai_cache::BeatmapInfoWithPP, OsuEnv};
#[derive(Debug, Clone)]
pub enum EmbedType {
Beatmap(Box<Beatmap>, BeatmapInfoWithPP, Mods),
Beatmapset(Vec<Beatmap>),
Beatmap(Box<Beatmap>, Option<Mode>, BeatmapInfoWithPP, Mods),
Beatmapset(Vec<Beatmap>, Option<Mode>),
}
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<Mode>,
}
lazy_static! {
@ -66,7 +75,6 @@ pub fn parse_old_links<'a>(
Ok(ToPrint {
embed,
link: capture.get(0).unwrap().as_str(),
mode,
})
})
.collect::<stream::FuturesUnordered<_>>()
@ -104,7 +112,7 @@ pub fn parse_new_links<'a>(
.await
}
}?;
Ok(ToPrint { embed, link, mode })
Ok(ToPrint { embed, link })
})
.collect::<stream::FuturesUnordered<_>>()
.filter_map(|v: Result<ToPrint>| 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::<stream::FuturesUnordered<_>>()
.filter_map(|v: Result<ToPrint>| 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<Mode>,
@ -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<Mode>,
) -> Result<Self> {
Ok(Self::Beatmapset(
env.beatmaps.get_beatmapset(beatmapset_id, mode).await?,
mode,
))
}
}

View file

@ -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<Mode>);
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<bool> {
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<EmbedType> {
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<Message>>,
) -> Option<(BeatmapWithMode, Option<Mods>)> {
use link_parser::{parse_short_links, EmbedType};
if let Some(replied) = referenced {
req: LoadRequest,
) -> Option<EmbedType> {
/* If the request is Beatmapset, we keep a fallback match on beatmap, and later convert it to a beatmapset. */
let mut fallback: Option<EmbedType> = None;
async fn collect_referenced(
env: &OsuEnv,
fallback: &mut Option<EmbedType>,
req: LoadRequest,
replied: &impl Borrow<Message>,
) -> Option<EmbedType> {
use link_parser::*;
async fn try_content(
env: &OsuEnv,
req: LoadRequest,
fallback: &mut Option<EmbedType>,
content: &str,
) -> Option<(BeatmapWithMode, Option<Mods>)> {
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<EmbedType> {
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::<OsuEnv>().unwrap().clone();
let b = load_beatmap(&env, msg.channel_id, msg.referenced_message.as_ref()).await;
let beatmapset = args.find::<OptBeatmapSet>().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::<UnparsedMods>().ok();
match b {
Some((bm, mods_def)) => {
let mods = args.find::<UnparsedMods>().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::<OsuEnv>().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::<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, &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<UnparsedMods>,
user: &UserHeader,
) -> Result<Vec<Score>> {
let osu_client = &env.client;
async fn fetch_for_beatmap(
env: &OsuEnv,
b: &Beatmap,
mode_override: Option<Mode>,
mods: &Option<UnparsedMods>,
user: &UserHeader,
) -> Result<Vec<Score>> {
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::<FuturesUnordered<_>>()
.try_collect::<Vec<_>>()
.await?
.into_iter()
.flatten()
.collect::<Vec<_>>();
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::<FuturesUnordered<_>>()
.try_collect::<Vec<_>>()
.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::<f64>())
.floor() as i64)
}
pub(crate) fn time_before_now(time: &chrono::DateTime<Utc>) -> 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())
}
}

View file

@ -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::<ScoreListStyle>().unwrap_or_default();
let guild = msg.guild_id.expect("Guild-only command");
let env = ctx.data.read().await.get::<OsuEnv>().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<String>,
pub beatmap: Arc<Beatmap>,
pub score: Score,
pub star: f64,
}
pub async fn get_leaderboard(
async fn get_leaderboard(
ctx: &Context,
env: &OsuEnv,
bm: &BeatmapWithMode,
beatmaps: impl IntoIterator<Item = Beatmap>,
mode_override: Option<Mode>,
show_unranked: bool,
order: OrderBy,
guild: GuildId,
) -> Result<Vec<Ranking>> {
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::<stream::FuturesOrdered<_>>()
.try_collect::<Vec<_>>()
.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::<FuturesUnordered<_>>()
.filter_map(future::ready)
.collect::<Vec<_>>()
.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<Mode>,
show_unranked: bool,
order: OrderBy,
guild: GuildId,
) -> Result<(Vec<Ranking>, 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<Ranking>,
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::<Vec<_>>();
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?;

View file

@ -452,7 +452,7 @@ impl Beatmap {
pub fn mention(&self, override_mode: Option<Mode>, mods: &Mods) -> String {
format!(
"[`{}`]({})",
"[`{}`](<{}>)",
self.short_link(override_mode, mods),
self.mode_link(override_mode),
)