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
parent 08e80fb816
commit 4a2f81c3d3
Signed by: nki
GPG key ID: 55A032EB38B49ADB
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 struct LastBeatmap {
pub channel_id: i64, pub channel_id: i64,
pub beatmap: Vec<u8>, pub beatmap: Vec<u8>,
pub mode: u8, pub mode: Option<u8>,
} }
impl LastBeatmap { impl LastBeatmap {

View file

@ -304,7 +304,7 @@ impl<'a> CollectedScore<'a> {
.get_beatmap_default(self.score.beatmap_id) .get_beatmap_default(self.score.beatmap_id)
.await?; .await?;
let content = env.oppai.get_beatmap(beatmap.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( async fn send_message_to(

View file

@ -7,6 +7,7 @@ use embeds::ScoreEmbedBuilder;
use link_parser::EmbedType; use link_parser::EmbedType;
use poise::{ChoiceParameter, CreateReply}; use poise::{ChoiceParameter, CreateReply};
use serenity::all::User; use serenity::all::User;
use server_rank::get_leaderboard_from_embed;
/// osu!-related command group. /// osu!-related command group.
#[poise::command( #[poise::command(
@ -260,7 +261,7 @@ async fn handle_listing<U: HasOsuEnv>(
let beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?; let beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?;
let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?; let content = env.oppai.get_beatmap(beatmap.beatmap_id).await?;
let beatmap = BeatmapWithMode(beatmap, mode); let beatmap = BeatmapWithMode(beatmap, Some(mode));
ctx.send({ ctx.send({
CreateReply::default() CreateReply::default()
@ -297,7 +298,7 @@ async fn handle_listing<U: HasOsuEnv>(
.into_message() .into_message()
.await?; .await?;
style style
.display_scores(plays, mode, ctx.serenity_context(), ctx.guild_id(), reply) .display_scores(plays, ctx.serenity_context(), ctx.guild_id(), reply)
.await?; .await?;
} }
} }
@ -321,24 +322,24 @@ async fn beatmap<U: HasOsuEnv>(
// override mods and mode if needed // override mods and mode if needed
match beatmap { match beatmap {
EmbedType::Beatmap(beatmap, info, bmmods) => { EmbedType::Beatmap(beatmap, bmode, info, bmmods) => {
let (beatmap, info, mods) = if mods.is_none() && mode.is_none_or(|v| v == beatmap.mode) let (beatmap, info, mods) =
{ if mods.is_none() && mode.is_none_or(|v| v == bmode.unwrap_or(beatmap.mode)) {
(*beatmap, info, bmmods) (*beatmap, info, bmmods)
} else { } else {
let mode = mode.unwrap_or(beatmap.mode); let mode = bmode.unwrap_or(beatmap.mode);
let mods = match mods { let mods = match mods {
None => bmmods, None => bmmods,
Some(mods) => mods.to_mods(mode)?, 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( ctx.send(
CreateReply::default() CreateReply::default()
.content(format!("Information for {}", beatmap.mention(mode, &mods))) .content(format!("Information for {}", beatmap.mention(mode, &mods)))
@ -355,9 +356,14 @@ async fn beatmap<U: HasOsuEnv>(
) )
.await?; .await?;
let bmode = beatmap.mode.with_override(mode); 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 b0 = &vec[0];
let msg = ctx let msg = ctx
.clone() .clone()
@ -438,35 +444,11 @@ async fn check<U: HasOsuEnv>(
ctx.defer().await?; ctx.defer().await?;
let embed = parse_map_input(ctx.channel_id(), env, map, mode, beatmapset).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 { let display = embed.mention();
beatmaps[0].0.mention(None, Mods::NOMOD)
} else {
beatmaps[0].0.beatmapset_mention()
};
let ordering = sort.unwrap_or_default(); 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() { if scores.is_empty() {
ctx.reply(format!( ctx.reply(format!(
"No plays found for {} on {} with the required criteria.", "No plays found for {} on {} with the required criteria.",
@ -499,13 +481,7 @@ async fn check<U: HasOsuEnv>(
}); });
style style
.display_scores( .display_scores(scores, ctx.serenity_context(), ctx.guild_id(), msg)
scores,
beatmaps[0].1,
ctx.serenity_context(),
ctx.guild_id(),
msg,
)
.await?; .await?;
Ok(()) Ok(())
@ -552,23 +528,20 @@ async fn leaderboard<U: HasOsuEnv>(
let env = ctx.data().osu_env(); let env = ctx.data().osu_env();
let guild = ctx.partial_guild().await.unwrap(); let guild = ctx.partial_guild().await.unwrap();
let style = style.unwrap_or_default(); 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? { let embed = 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")),
};
ctx.defer().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(), ctx.serenity_context(),
env, &env,
&bm, embed,
unranked.unwrap_or(false), None,
sort.unwrap_or(server_rank::OrderBy::PP), unranked.unwrap_or(true),
order,
guild.id, guild.id,
) )
.await?; .await?;
@ -576,12 +549,10 @@ async fn leaderboard<U: HasOsuEnv>(
scores.reverse(); scores.reverse();
} }
let beatmap = &bm.0;
if scores.is_empty() { if scores.is_empty() {
ctx.reply(format!( ctx.reply(format!(
"No scores have been recorded in **{}** on {}.", "No scores have been recorded in **{}** on {}.",
guild.name, guild.name, scoreboard_msg,
beatmap.mention(mode, Mods::NOMOD),
)) ))
.await?; .await?;
return Ok(()); return Ok(());
@ -589,8 +560,7 @@ async fn leaderboard<U: HasOsuEnv>(
let header = format!( let header = format!(
"Here are the top scores of **{}** on {}", "Here are the top scores of **{}** on {}",
guild.name, guild.name, scoreboard_msg,
beatmap.mention(mode, Mods::NOMOD),
); );
match style { match style {
@ -600,7 +570,7 @@ async fn leaderboard<U: HasOsuEnv>(
ctx.serenity_context(), ctx.serenity_context(),
reply, reply,
scores, scores,
&bm, show_diff,
sort.unwrap_or_default(), sort.unwrap_or_default(),
) )
.await?; .await?;
@ -610,7 +580,6 @@ async fn leaderboard<U: HasOsuEnv>(
style style
.display_scores( .display_scores(
scores.into_iter().map(|s| s.score).collect(), scores.into_iter().map(|s| s.score).collect(),
bm.1,
ctx.serenity_context(), ctx.serenity_context(),
Some(guild.id), Some(guild.id),
reply, reply,
@ -659,18 +628,10 @@ async fn parse_map_input(
) -> Result<EmbedType> { ) -> Result<EmbedType> {
let output = match input { let output = match input {
None => { None => {
let Some((BeatmapWithMode(b, mode), bmmods)) = let Some(v) = load_beatmap_from_channel(env, channel_id).await else {
load_beatmap(env, channel_id, None as Option<&'_ Message>).await
else {
return Err(Error::msg("no beatmap mentioned in this channel")); return Err(Error::msg("no beatmap mentioned in this channel"));
}; };
let mods = bmmods.unwrap_or_else(|| Mods::NOMOD.clone()); v
let info = env
.oppai
.get_beatmap(b.beatmap_id)
.await?
.get_possible_pp_with(mode, &mods);
EmbedType::Beatmap(Box::new(b), info, mods)
} }
Some(map) => { Some(map) => {
if let Ok(id) = map.parse::<u64>() { if let Ok(id) = map.parse::<u64>() {
@ -685,6 +646,7 @@ async fn parse_map_input(
.get_possible_pp_with(beatmap.mode, Mods::NOMOD); .get_possible_pp_with(beatmap.mode, Mods::NOMOD);
return Ok(EmbedType::Beatmap( return Ok(EmbedType::Beatmap(
Box::new(beatmap), Box::new(beatmap),
None,
info, info,
Mods::NOMOD.clone(), Mods::NOMOD.clone(),
)); ));
@ -708,14 +670,14 @@ async fn parse_map_input(
// override into beatmapset if needed // override into beatmapset if needed
let output = if beatmapset == Some(true) { let output = if beatmapset == Some(true) {
match output { match output {
EmbedType::Beatmap(beatmap, _, _) => { EmbedType::Beatmap(beatmap, _, _, _) => {
let beatmaps = env let beatmaps = env
.beatmaps .beatmaps
.get_beatmapset(beatmap.beatmapset_id, mode) .get_beatmapset(beatmap.beatmapset_id, mode)
.await?; .await?;
EmbedType::Beatmapset(beatmaps) EmbedType::Beatmapset(beatmaps, mode)
} }
bm @ EmbedType::Beatmapset(_) => bm, bm @ EmbedType::Beatmapset(_, _) => bm,
} }
} else { } else {
output output

View file

@ -81,11 +81,17 @@ impl OsuLastBeatmap {
} }
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 = let last_beatmap =
models::LastBeatmap::by_channel_id(id.into().get() as i64, &self.0).await?; models::LastBeatmap::by_channel_id(id.into().get() as i64, &self.0).await?;
Ok(match last_beatmap { 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, None => None,
}) })
} }
@ -94,12 +100,12 @@ impl OsuLastBeatmap {
&self, &self,
channel: impl Into<ChannelId>, channel: impl Into<ChannelId>,
beatmap: &Beatmap, beatmap: &Beatmap,
mode: Mode, mode: Option<Mode>,
) -> Result<()> { ) -> Result<()> {
let b = models::LastBeatmap { let b = models::LastBeatmap {
channel_id: channel.into().get() as i64, channel_id: channel.into().get() as i64,
beatmap: bincode::serialize(beatmap)?, beatmap: bincode::serialize(beatmap)?,
mode: mode as u8, mode: mode.map(|mode| mode as u8),
}; };
b.store(&self.0).await?; b.store(&self.0).await?;
Ok(()) Ok(())

View file

@ -7,7 +7,7 @@ mod scores {
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::models::{Mode, Score}; use crate::models::Score;
#[derive(Debug, Clone, Copy, PartialEq, Eq, ChoiceParameter)] #[derive(Debug, Clone, Copy, PartialEq, Eq, ChoiceParameter)]
/// The style for the scores list to be displayed. /// The style for the scores list to be displayed.
@ -40,16 +40,13 @@ mod scores {
pub async fn display_scores( pub async fn display_scores(
self, self,
scores: Vec<Score>, scores: Vec<Score>,
mode: Mode,
ctx: &Context, ctx: &Context,
guild_id: Option<GuildId>, guild_id: Option<GuildId>,
m: Message, m: Message,
) -> Result<()> { ) -> Result<()> {
match self { match self {
ScoreListStyle::Table => table::display_scores_table(scores, mode, ctx, m).await, ScoreListStyle::Table => table::display_scores_table(scores, ctx, m).await,
ScoreListStyle::Grid => { ScoreListStyle::Grid => grid::display_scores_grid(scores, ctx, guild_id, m).await,
grid::display_scores_grid(scores, mode, ctx, guild_id, m).await
}
} }
} }
} }
@ -64,11 +61,10 @@ mod scores {
use crate::discord::interaction::score_components; use crate::discord::interaction::score_components;
use crate::discord::{cache::save_beatmap, BeatmapWithMode, OsuEnv}; use crate::discord::{cache::save_beatmap, BeatmapWithMode, OsuEnv};
use crate::models::{Mode, Score}; use crate::models::Score;
pub async fn display_scores_grid( pub async fn display_scores_grid(
scores: Vec<Score>, scores: Vec<Score>,
mode: Mode,
ctx: &Context, ctx: &Context,
guild_id: Option<GuildId>, guild_id: Option<GuildId>,
mut on: Message, mut on: Message,
@ -80,11 +76,7 @@ mod scores {
} }
paginate_with_first_message( paginate_with_first_message(
Paginate { Paginate { scores, guild_id },
scores,
guild_id,
mode,
},
ctx, ctx,
on, on,
std::time::Duration::from_secs(60), std::time::Duration::from_secs(60),
@ -96,7 +88,6 @@ mod scores {
pub struct Paginate { pub struct Paginate {
scores: Vec<Score>, scores: Vec<Score>,
guild_id: Option<GuildId>, guild_id: Option<GuildId>,
mode: Mode,
} }
#[async_trait] #[async_trait]
@ -107,9 +98,16 @@ mod scores {
let score = &self.scores[page]; let score = &self.scores[page];
let hourglass = msg.react(ctx, '⌛').await?; let hourglass = msg.react(ctx, '⌛').await?;
let mode = self.mode; let beatmap = env
let beatmap = env.beatmaps.get_beatmap(score.beatmap_id, mode).await?; .beatmaps
.get_beatmap(score.beatmap_id, score.mode)
.await?;
let content = env.oppai.get_beatmap(beatmap.beatmap_id).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 bm = BeatmapWithMode(beatmap, mode);
let user = env let user = env
.client .client
@ -153,12 +151,11 @@ mod scores {
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::discord::oppai_cache::Stats; use crate::discord::oppai_cache::Stats;
use crate::discord::{Beatmap, BeatmapInfo, OsuEnv}; use crate::discord::{time_before_now, Beatmap, BeatmapInfo, OsuEnv};
use crate::models::{Mode, Score}; use crate::models::Score;
pub async fn display_scores_table( pub async fn display_scores_table(
scores: Vec<Score>, scores: Vec<Score>,
mode: Mode,
ctx: &Context, ctx: &Context,
mut on: Message, mut on: Message,
) -> Result<()> { ) -> Result<()> {
@ -172,7 +169,6 @@ mod scores {
Paginate { Paginate {
header: on.content.clone(), header: on.content.clone(),
scores, scores,
mode,
}, },
ctx, ctx,
on, on,
@ -185,7 +181,6 @@ mod scores {
pub struct Paginate { pub struct Paginate {
header: String, header: String,
scores: Vec<Score>, scores: Vec<Score>,
mode: Mode,
} }
impl Paginate { impl Paginate {
@ -212,14 +207,13 @@ mod scores {
let hourglass = msg.react(ctx, '⌛').await?; let hourglass = msg.react(ctx, '⌛').await?;
let plays = &self.scores[start..end]; let plays = &self.scores[start..end];
let mode = self.mode;
let beatmaps = plays let beatmaps = plays
.iter() .iter()
.map(|play| async move { .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 info = {
let b = oppai.get_beatmap(beatmap.beatmap_id).await?; 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)> Ok((beatmap, info)) as Result<(Beatmap, BeatmapInfo)>
}) })
@ -235,7 +229,7 @@ mod scores {
None => { None => {
let b = oppai.get_beatmap(p.beatmap_id).await?; let b = oppai.get_beatmap(p.beatmap_id).await?;
let pp = b.get_pp_from( let pp = b.get_pp_from(
mode, p.mode,
Some(p.max_combo), Some(p.max_combo),
Stats::Raw(&p.statistics), Stats::Raw(&p.statistics),
&p.mods, &p.mods,
@ -289,15 +283,16 @@ mod scores {
beatmap.artist, beatmap.artist,
beatmap.title, beatmap.title,
beatmap.difficulty_name, 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()) .unwrap_or_else(|| "FETCH_FAILED".to_owned())
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
const SCORE_HEADERS: [&str; 6] = ["#", "PP", "Acc", "Ranks", "Mods", "Beatmap"]; const SCORE_HEADERS: [&str; 7] =
const SCORE_ALIGNS: [Align; 6] = [Right, Right, Right, Right, Right, Left]; ["#", "PP", "Acc", "Ranks", "Mods", "When", "Beatmap"];
const SCORE_ALIGNS: [Align; 7] = [Right, Right, Right, Right, Right, Right, Left];
let score_arr = plays let score_arr = plays
.iter() .iter()
@ -308,9 +303,10 @@ mod scores {
[ [
format!("{}", id + start + 1), format!("{}", id + start + 1),
pp.to_string(), pp.to_string(),
format!("{:.2}%", play.accuracy(self.mode)), format!("{:.2}%", play.accuracy(play.mode)),
format!("{}", rank), format!("{}", rank),
play.mods.to_string(), play.mods.to_string(),
time_before_now(&play.date),
beatmap.clone(), beatmap.clone(),
] ]
}) })
@ -486,7 +482,7 @@ mod beatmapset {
save_beatmap( save_beatmap(
&env, &env,
msg.channel_id, msg.channel_id,
&BeatmapWithMode(map.clone(), self.mode.unwrap_or(map.mode)), &BeatmapWithMode(map.clone(), self.mode),
) )
.await .await
.pls_ok(); .pls_ok();

View file

@ -506,7 +506,9 @@ impl<'a> FakeScore<'a> {
pub fn embed(self, ctx: &Context) -> Result<CreateEmbed> { pub fn embed(self, ctx: &Context) -> Result<CreateEmbed> {
let BeatmapWithMode(b, mode) = self.bm; 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 { let attrs = match &info.attrs {
rosu_pp::any::PerformanceAttributes::Osu(osu_performance_attributes) => { rosu_pp::any::PerformanceAttributes::Osu(osu_performance_attributes) => {
osu_performance_attributes osu_performance_attributes
@ -540,7 +542,7 @@ impl<'a> FakeScore<'a> {
"".into() "".into()
} else { } else {
let pp = self.content.get_pp_from( let pp = self.content.get_pp_from(
*mode, mode.unwrap_or(b.mode),
None, None,
Stats::AccOnly { Stats::AccOnly {
acc: accuracy, acc: accuracy,
@ -594,7 +596,7 @@ impl<'a> FakeScore<'a> {
"Map stats", "Map stats",
b.difficulty b.difficulty
.apply_mods(&self.mods, attrs.stars()) .apply_mods(&self.mods, attrs.stars())
.format_info(*mode, &self.mods, b), .format_info(mode.unwrap_or(b.mode), &self.mods, b),
false, false,
) )
.footer(CreateEmbedFooter::new( .footer(CreateEmbedFooter::new(
@ -707,7 +709,7 @@ pub(crate) fn user_embed(u: User, ex: UserExtras) -> CreateEmbed {
"> {}", "> {}",
map.difficulty map.difficulty
.apply_mods(&v.mods, info.attrs.stars()) .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> ") .replace('\n', "\n> ")
)) ))
.build(), .build(),

View file

@ -59,7 +59,7 @@ pub fn score_hook<'a>(
let mode = score.mode; let mode = score.mode;
let content = env.oppai.get_beatmap(score.beatmap_id).await?; let content = env.oppai.get_beatmap(score.beatmap_id).await?;
let header = env.client.user_header(score.user_id).await?.unwrap(); 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::<FuturesOrdered<_>>()
.collect::<Vec<_>>() .collect::<Vec<_>>()
@ -253,11 +253,10 @@ pub fn hook<'a>(
to_join to_join
.then(|l| async move { .then(|l| async move {
match l.embed { match l.embed {
EmbedType::Beatmap(b, info, mods) => { EmbedType::Beatmap(b, mode, info, mods) => {
handle_beatmap(ctx, &b, info, l.link, l.mode, mods, msg) handle_beatmap(ctx, &b, info, l.link, mode, mods, msg)
.await .await
.pls_ok(); .pls_ok();
let mode = l.mode.unwrap_or(b.mode);
let bm = super::BeatmapWithMode(*b, mode); let bm = super::BeatmapWithMode(*b, mode);
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone(); let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
@ -266,10 +265,8 @@ pub fn hook<'a>(
.await .await
.pls_ok(); .pls_ok();
} }
EmbedType::Beatmapset(b) => { EmbedType::Beatmapset(b, mode) => {
handle_beatmapset(ctx, b, l.link, l.mode, msg) handle_beatmapset(ctx, b, l.link, mode, msg).await.pls_ok();
.await
.pls_ok();
} }
} }
}) })
@ -289,7 +286,6 @@ async fn handle_beatmap<'a, 'b>(
mods: Mods, mods: Mods,
reply_to: &Message, reply_to: &Message,
) -> Result<()> { ) -> Result<()> {
let mode = mode.unwrap_or(beatmap.mode);
reply_to reply_to
.channel_id .channel_id
.send_message( .send_message(
@ -299,10 +295,21 @@ async fn handle_beatmap<'a, 'b>(
MessageBuilder::new() MessageBuilder::new()
.push("Beatmap information for ") .push("Beatmap information for ")
.push_mono_safe(link) .push_mono_safe(link)
.push(" (")
.push(beatmap.mention(mode, &mods))
.push(")")
.build(), .build(),
) )
.embed(beatmap_embed(beatmap, mode, &mods, &info)) .embed(beatmap_embed(
.components(vec![beatmap_components(mode, reply_to.guild_id)]) 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), .reference_message(reply_to),
) )
.await?; .await?;
@ -317,7 +324,14 @@ async fn handle_beatmapset<'a, 'b>(
reply_to: &Message, reply_to: &Message,
) -> Result<()> { ) -> Result<()> {
let reply = reply_to let reply = reply_to
.reply(ctx, format!("Beatmapset information for `{}`", link)) .reply(
ctx,
format!(
"Beatmapset information for `{}` ({})",
link,
beatmaps[0].beatmapset_mention()
),
)
.await?; .await?;
crate::discord::display::display_beatmapset( crate::discord::display::display_beatmapset(
ctx.clone(), ctx.clone(),

View file

@ -14,8 +14,9 @@ use crate::{discord::embeds::FakeScore, mods::UnparsedMods, Mode, Mods, UserHead
use super::{ use super::{
display::ScoreListStyle, display::ScoreListStyle,
embeds::beatmap_embed, embeds::beatmap_embed,
server_rank::{display_rankings_table, get_leaderboard, OrderBy}, link_parser::EmbedType,
BeatmapWithMode, OsuEnv, server_rank::{display_rankings_table, get_leaderboard_from_embed, OrderBy},
BeatmapWithMode, LoadRequest, OsuEnv,
}; };
pub(super) const BTN_CHECK: &str = "youmubot_osu_btn_check"; 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 msg = &*comp.message;
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone(); 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 .await
.unwrap(); .unwrap();
let user = match env.saved_users.by_user_id(comp.user.id).await? { 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 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() { if scores.is_empty() {
comp.create_followup( comp.create_followup(
&ctx, &ctx,
CreateInteractionResponseFollowup::new().content(format!( 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.username,
user.id, user.id,
bm.short_link(Mods::NOMOD) embed.mention(),
)), )),
) )
.await?; .await?;
@ -98,10 +99,10 @@ pub fn handle_check_button<'a>(
.create_followup( .create_followup(
&ctx, &ctx,
CreateInteractionResponseFollowup::new().content(format!( 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.username,
user.id, user.id,
bm.short_link(Mods::NOMOD) embed.mention()
)), )),
) )
.await?; .await?;
@ -110,7 +111,7 @@ pub fn handle_check_button<'a>(
let guild_id = comp.guild_id; let guild_id = comp.guild_id;
spawn_future(async move { spawn_future(async move {
ScoreListStyle::Grid ScoreListStyle::Grid
.display_scores(scores, bm.1, &ctx, guild_id, reply) .display_scores(scores, &ctx, guild_id, reply)
.await .await
.pls_ok(); .pls_ok();
}); });
@ -189,11 +190,16 @@ pub fn handle_simulate_button<'a>(
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone(); 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 .await
.unwrap(); .unwrap();
let b = &bm.0; let (b, mode) = match embed {
let mode = bm.1; 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 content = env.oppai.get_beatmap(b.beatmap_id).await?;
let info = content.get_info_with(mode, Mods::NOMOD); let info = content.get_info_with(mode, Mods::NOMOD);
@ -227,7 +233,9 @@ pub fn handle_simulate_button<'a>(
query.interaction.defer(&ctx).await?; 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 query
.interaction .interaction
.create_followup( .create_followup(
@ -251,7 +259,7 @@ async fn handle_simluate_query(
bm: BeatmapWithMode, bm: BeatmapWithMode,
) -> Result<()> { ) -> Result<()> {
let b = &bm.0; 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 content = env.oppai.get_beatmap(b.beatmap_id).await?;
let score: FakeScore = { let score: FakeScore = {
@ -305,54 +313,58 @@ async fn handle_last_req(
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone(); let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let (bm, mods_def) = super::load_beatmap(&env, comp.channel_id, Some(msg)) let embed = super::load_beatmap(
.await &env,
.unwrap(); comp.channel_id,
let BeatmapWithMode(b, m) = &bm; Some(msg),
if is_beatmapset_req {
LoadRequest::Beatmapset
} else {
LoadRequest::Any
},
)
.await
.unwrap();
let mods = mods_def.unwrap_or_default(); match embed {
EmbedType::Beatmapset(beatmapset, mode) => {
if is_beatmapset_req { let reply = comp
let beatmapset = env .create_followup(
.beatmaps &ctx,
.get_beatmapset(bm.0.beatmapset_id, None) CreateInteractionResponseFollowup::new().content(format!(
.await?; "Beatmapset `{}`",
let reply = comp beatmapset[0].beatmapset_mention()
.create_followup( )),
&ctx, )
CreateInteractionResponseFollowup::new() .await?;
.content(format!("Beatmapset `{}`", bm.0.beatmapset_mention())), super::display::display_beatmapset(
ctx.clone(),
beatmapset,
mode,
None,
comp.guild_id,
reply,
) )
.await?; .await?;
super::display::display_beatmapset( return Ok(());
ctx.clone(), }
beatmapset, EmbedType::Beatmap(b, m, _, mods) => {
None, let info = env
None, .oppai
comp.guild_id, .get_beatmap(b.beatmap_id)
reply, .await?
) .get_possible_pp_with(m.unwrap_or(b.mode), &mods);
.await?; comp.create_followup(
return Ok(()); &ctx,
} else { serenity::all::CreateInteractionResponseFollowup::new()
let info = env .content(format!("Information for beatmap {}", b.mention(m, &mods)))
.oppai .embed(beatmap_embed(&*b, m.unwrap_or(b.mode), &mods, &info))
.get_beatmap(b.beatmap_id) .components(vec![beatmap_components(m.unwrap_or(b.mode), comp.guild_id)]),
.await? )
.get_possible_pp_with(*m, &mods); .await?;
comp.create_followup( // Save the beatmap...
&ctx, super::cache::save_beatmap(&env, msg.channel_id, &BeatmapWithMode(*b, m)).await?;
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?;
} }
Ok(()) Ok(())
@ -380,20 +392,23 @@ pub fn handle_lb_button<'a>(
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone(); 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 .await
.unwrap(); .unwrap();
let order = OrderBy::default(); let order = OrderBy::default();
let guild = comp.guild_id.expect("Guild-only command"); 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() { if scores.is_empty() {
comp.create_followup( comp.create_followup(
&ctx, &ctx,
CreateInteractionResponseFollowup::new().content( CreateInteractionResponseFollowup::new().content(format!(
"No scores have been recorded for this beatmap from anyone in this server.", "No scores have been recorded for {} from anyone in this server.",
), scoreboard_msg
)),
) )
.await?; .await?;
return Ok(()); return Ok(());
@ -402,13 +417,11 @@ pub fn handle_lb_button<'a>(
let reply = comp let reply = comp
.create_followup( .create_followup(
&ctx, &ctx,
CreateInteractionResponseFollowup::new().content(format!( CreateInteractionResponseFollowup::new()
"Here are the top scores on beatmap `{}`!", .content(format!("Here are the top scores on {}!", scoreboard_msg)),
bm.short_link(Mods::NOMOD)
)),
) )
.await?; .await?;
display_rankings_table(ctx, reply, scores, &bm, order).await?; display_rankings_table(ctx, reply, scores, show_diff, order).await?;
Ok(()) Ok(())
}) })
} }

View file

@ -9,15 +9,24 @@ use youmubot_prelude::*;
use super::{oppai_cache::BeatmapInfoWithPP, OsuEnv}; use super::{oppai_cache::BeatmapInfoWithPP, OsuEnv};
#[derive(Debug, Clone)]
pub enum EmbedType { pub enum EmbedType {
Beatmap(Box<Beatmap>, BeatmapInfoWithPP, Mods), Beatmap(Box<Beatmap>, Option<Mode>, BeatmapInfoWithPP, Mods),
Beatmapset(Vec<Beatmap>), 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 struct ToPrint<'a> {
pub embed: EmbedType, pub embed: EmbedType,
pub link: &'a str, pub link: &'a str,
pub mode: Option<Mode>,
} }
lazy_static! { lazy_static! {
@ -66,7 +75,6 @@ pub fn parse_old_links<'a>(
Ok(ToPrint { Ok(ToPrint {
embed, embed,
link: capture.get(0).unwrap().as_str(), link: capture.get(0).unwrap().as_str(),
mode,
}) })
}) })
.collect::<stream::FuturesUnordered<_>>() .collect::<stream::FuturesUnordered<_>>()
@ -104,7 +112,7 @@ pub fn parse_new_links<'a>(
.await .await
} }
}?; }?;
Ok(ToPrint { embed, link, mode }) Ok(ToPrint { embed, link })
}) })
.collect::<stream::FuturesUnordered<_>>() .collect::<stream::FuturesUnordered<_>>()
.filter_map(|v: Result<ToPrint>| future::ready(v.pls_ok())) .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?, "s" => EmbedType::from_beatmapset_id(env, id, mode).await?,
_ => unreachable!(), _ => unreachable!(),
}; };
Ok(ToPrint { embed, link, mode }) Ok(ToPrint { embed, link })
}) })
.collect::<stream::FuturesUnordered<_>>() .collect::<stream::FuturesUnordered<_>>()
.filter_map(|v: Result<ToPrint>| future::ready(v.pls_ok())) .filter_map(|v: Result<ToPrint>| future::ready(v.pls_ok()))
} }
impl EmbedType { impl EmbedType {
async fn from_beatmap_id( pub(crate) async fn from_beatmap_id(
env: &OsuEnv, env: &OsuEnv,
beatmap_id: u64, beatmap_id: u64,
mode: Option<Mode>, mode: Option<Mode>,
@ -158,16 +166,17 @@ impl EmbedType {
.await .await
.map(|b| b.get_possible_pp_with(mode, &mods))? .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, env: &OsuEnv,
beatmapset_id: u64, beatmapset_id: u64,
mode: Option<Mode>, mode: Option<Mode>,
) -> Result<Self> { ) -> Result<Self> {
Ok(Self::Beatmapset( Ok(Self::Beatmapset(
env.beatmaps.get_beatmapset(beatmapset_id, mode).await?, 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 chrono::Utc;
use futures_util::join; use futures_util::join;
use interaction::{beatmap_components, score_components}; use interaction::{beatmap_components, score_components};
use link_parser::EmbedType;
use oppai_cache::BeatmapInfoWithPP; use oppai_cache::BeatmapInfoWithPP;
use rand::seq::IteratorRandom; use rand::seq::IteratorRandom;
use serenity::{ use serenity::{
@ -224,15 +226,11 @@ pub async fn mania(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode); pub(crate) struct BeatmapWithMode(pub Beatmap, pub Option<Mode>);
impl BeatmapWithMode { impl BeatmapWithMode {
pub fn short_link(&self, mods: &Mods) -> String {
self.0.short_link(Some(self.1), mods)
}
fn mode(&self) -> Mode { 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; let osu_client = &env.client;
async fn check(client: &OsuHttpClient, u: &User, mode: Mode, map_id: u64) -> Result<bool> { async fn check(client: &OsuHttpClient, u: &User, mode: Mode, map_id: u64) -> Result<bool> {
Ok(client 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? .await?
.into_iter() .into_iter()
.take(1) .take(1)
@ -518,7 +516,7 @@ impl UserExtras {
.get_beatmap(s.beatmap_id) .get_beatmap(s.beatmap_id)
.await? .await?
.get_info_with(mode, &s.mods); .get_info_with(mode, &s.mods);
Some((s, BeatmapWithMode(beatmap, mode), info)) Some((s, BeatmapWithMode(beatmap, Some(mode)), info))
} else { } else {
None None
}; };
@ -691,7 +689,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
) )
.await?; .await?;
style style
.display_scores(plays, mode, ctx, reply.guild_id, reply) .display_scores(plays, ctx, reply.guild_id, reply)
.await?; .await?;
} }
Nth::Nth(nth) => { Nth::Nth(nth) => {
@ -705,7 +703,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
.count(); .count();
let beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?; let beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?;
let content = env.oppai.get_beatmap(beatmap.beatmap_id).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 msg.channel_id
.send_message( .send_message(
@ -764,7 +762,7 @@ pub async fn pins(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
) )
.await?; .await?;
style style
.display_scores(plays, mode, ctx, reply.guild_id, reply) .display_scores(plays, ctx, reply.guild_id, reply)
.await?; .await?;
} }
Nth::Nth(nth) => { 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 beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?;
let content = env.oppai.get_beatmap(beatmap.beatmap_id).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 msg.channel_id
.send_message( .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. /// Load the mentioned beatmap from the given message.
pub(crate) async fn load_beatmap( pub(crate) async fn load_beatmap(
env: &OsuEnv, env: &OsuEnv,
channel_id: serenity::all::ChannelId, channel_id: serenity::all::ChannelId,
referenced: Option<&impl Borrow<Message>>, referenced: Option<&impl Borrow<Message>>,
) -> Option<(BeatmapWithMode, Option<Mods>)> { req: LoadRequest,
use link_parser::{parse_short_links, EmbedType}; ) -> Option<EmbedType> {
if let Some(replied) = referenced { /* 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( async fn try_content(
env: &OsuEnv, env: &OsuEnv,
req: LoadRequest,
fallback: &mut Option<EmbedType>,
content: &str, content: &str,
) -> Option<(BeatmapWithMode, Option<Mods>)> { ) -> Option<EmbedType> {
let tp = parse_short_links(env, content).next().await?; parse_short_links(env, content)
match tp.embed { .filter(|e| {
EmbedType::Beatmap(b, _, mods) => { future::ready(match &e.embed {
let mode = tp.mode.unwrap_or(b.mode); EmbedType::Beatmap(_, _, _, _) => {
Some((BeatmapWithMode(*b, mode), Some(mods))) if fallback.is_none() {
} fallback.replace(e.embed.clone());
_ => None, }
} 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 embed in &replied.borrow().embeds {
for field in &embed.fields { 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); return Some(v);
} }
} }
if let Some(desc) = &embed.description { 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); return Some(v);
} }
} }
} }
if let Some(v) = try_content(env, &replied.borrow().content).await { None
return Some(v);
}
} }
let b = cache::get_beatmap(env, channel_id).await.ok().flatten(); let embed = match referenced {
b.map(|b| (b, None)) 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] #[command]
@ -858,58 +917,56 @@ pub(crate) async fn load_beatmap(
pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone(); 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 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 { let content_type = embed.mention();
Some((bm, mods_def)) => { match embed {
let mods = args.find::<UnparsedMods>().ok(); EmbedType::Beatmap(b, mode_, _, mods) => {
if beatmapset { let mode = mode_.unwrap_or(b.mode);
let beatmapset = env let mods = match umods {
.beatmaps Some(m) => m.to_mods(mode)?,
.get_beatmapset( None => mods,
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 info = env let info = env
.oppai .oppai
.get_beatmap(bm.0.beatmap_id) .get_beatmap(b.beatmap_id)
.await? .await?
.get_possible_pp_with(bm.1, &mods); .get_possible_pp_with(mode, &mods);
msg.channel_id msg.channel_id
.send_message( .send_message(
&ctx, &ctx,
CreateMessage::new() CreateMessage::new()
.content("Here is the beatmap you requested!") .content(format!("Information for {}", content_type))
.embed(beatmap_embed(&bm.0, bm.1, &mods, &info)) .embed(beatmap_embed(&b, mode, &mods, &info))
.components(vec![beatmap_components(bm.1, msg.guild_id)]) .components(vec![beatmap_components(mode, msg.guild_id)])
.reference_message(msg), .reference_message(msg),
) )
.await?; .await?;
// Save the beatmap... // Save the beatmap...
cache::save_beatmap(&env, msg.channel_id, &bm).await?; cache::save_beatmap(&env, msg.channel_id, &BeatmapWithMode(*b, mode_)).await?;
} }
None => { EmbedType::Beatmapset(beatmaps, mode) => {
msg.reply(&ctx, "No beatmap was queried on this channel.") let reply = msg
.reply(&ctx, format!("Information for {}", content_type))
.await?;
display::display_beatmapset(ctx.clone(), beatmaps, mode, umods, msg.guild_id, reply)
.await?; .await?;
} }
} }
@ -924,26 +981,27 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
#[max_args(3)] #[max_args(3)]
pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult { pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone(); 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 Some(embed) = load_beatmap(
&env,
let bm = match bm { msg.channel_id,
Some((bm, _)) => bm, msg.referenced_message.as_ref(),
None => { LoadRequest::Any,
msg.reply(&ctx, "No beatmap queried on this channel.") )
.await?; .await
return Ok(()); else {
} msg.reply(&ctx, "No beatmap queried on this channel.")
.await?;
return Ok(());
}; };
let mode = bm.1;
let umods = args.find::<UnparsedMods>().ok(); let umods = args.find::<UnparsedMods>().ok();
let mods = umods.clone().unwrap_or_default().to_mods(mode)?;
let style = args let style = args
.single::<ScoreListStyle>() .single::<ScoreListStyle>()
.unwrap_or(ScoreListStyle::Grid); .unwrap_or(ScoreListStyle::Grid);
let username_arg = args.single::<UsernameArg>().ok(); let username_arg = args.single::<UsernameArg>().ok();
let (_, user) = user_header_from_args(username_arg, &env, msg).await?; 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() { if scores.is_empty() {
msg.reply(&ctx, "No scores found").await?; msg.reply(&ctx, "No scores found").await?;
@ -955,12 +1013,12 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
format!( format!(
"Here are the scores by `{}` on {}!", "Here are the scores by `{}` on {}!",
&user.username, &user.username,
bm.0.mention(Some(bm.1), &mods) embed.mention()
), ),
) )
.await?; .await?;
style style
.display_scores(scores, mode, ctx, msg.guild_id, reply) .display_scores(scores, ctx, msg.guild_id, reply)
.await?; .await?;
Ok(()) Ok(())
@ -968,30 +1026,41 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
pub(crate) async fn do_check( pub(crate) async fn do_check(
env: &OsuEnv, env: &OsuEnv,
bm: &[BeatmapWithMode], embed: &EmbedType,
mods: Option<UnparsedMods>, mods: Option<UnparsedMods>,
user: &UserHeader, user: &UserHeader,
) -> Result<Vec<Score>> { ) -> 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 let mut scores = match embed {
.iter() EmbedType::Beatmap(beatmap, mode, _, _) => {
.map(|bm| { fetch_for_beatmap(env, &**beatmap, *mode, &mods, user).await?
let BeatmapWithMode(b, m) = bm; }
let mods = mods.clone().and_then(|t| t.to_mods(*m).ok()); EmbedType::Beatmapset(vec, mode) => vec
osu_client .iter()
.scores(b.beatmap_id, |f| f.user(UserID::ID(user.id)).mode(*m)) .map(|b| fetch_for_beatmap(env, b, *mode, &mods, user))
.map_ok(move |mut v| { .collect::<FuturesUnordered<_>>()
v.retain(|s| mods.as_ref().is_none_or(|m| m.contains(&s.mods))); .try_collect::<Vec<_>>()
v .await?
}) .concat(),
}) };
.collect::<FuturesUnordered<_>>()
.try_collect::<Vec<_>>()
.await?
.into_iter()
.flatten()
.collect::<Vec<_>>();
scores.sort_by(|a, b| { scores.sort_by(|a, b| {
b.pp.unwrap_or(-1.0) b.pp.unwrap_or(-1.0)
.partial_cmp(&a.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 beatmap = env.beatmaps.get_beatmap(play.beatmap_id, mode).await?;
let content = env.oppai.get_beatmap(beatmap.beatmap_id).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 msg.channel_id
.send_message(&ctx, { .send_message(&ctx, {
@ -1061,7 +1130,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
) )
.await?; .await?;
style style
.display_scores(plays, mode, ctx, msg.guild_id, reply) .display_scores(plays, ctx, msg.guild_id, reply)
.await?; .await?;
} }
} }
@ -1182,3 +1251,20 @@ pub(in crate::discord) async fn calculate_weighted_map_age(
/ scales().iter().take(scores.len()).sum::<f64>()) / scales().iter().take(scores.len()).sum::<f64>())
.floor() as i64) .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::{ use crate::{
discord::{db::OsuUser, display::ScoreListStyle, oppai_cache::Stats, BeatmapWithMode}, discord::{
models::{Mode, Mods}, db::OsuUser, display::ScoreListStyle, link_parser::EmbedType, oppai_cache::Stats,
time_before_now,
},
models::Mode,
request::UserID, request::UserID,
Score, Beatmap, Score,
}; };
use super::{ModeArg, OsuEnv}; 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 style = args.single::<ScoreListStyle>().unwrap_or_default();
let guild = msg.guild_id.expect("Guild-only command"); let guild = msg.guild_id.expect("Guild-only command");
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone(); let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let Some((bm, _)) = let Some(beatmap) = super::load_beatmap(
super::load_beatmap(&env, msg.channel_id, msg.referenced_message.as_ref()).await &env,
msg.channel_id,
msg.referenced_message.as_ref(),
crate::discord::LoadRequest::Any,
)
.await
else { else {
msg.reply(&ctx, "No beatmap queried on this channel.") msg.reply(&ctx, "No beatmap queried on this channel.")
.await?; .await?;
return Ok(()); return Ok(());
}; };
let reaction = msg.react(ctx, '⌛').await?;
let scores = { let scoreboard_msg = beatmap.mention();
let reaction = msg.react(ctx, '⌛').await?; let (scores, show_diff) =
let s = get_leaderboard(ctx, &env, &bm, show_all, order, guild).await?; get_leaderboard_from_embed(ctx, &env, beatmap, None, show_all, order, guild).await?;
reaction.delete(&ctx).await?; reaction.delete(&ctx).await?;
s
};
if scores.is_empty() { if scores.is_empty() {
msg.reply(&ctx, "No scores have been recorded for this beatmap.") 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 let reply = msg
.reply( .reply(
&ctx, &ctx,
format!( format!("⌛ Loading top scores on {}...", scoreboard_msg),
"⌛ Loading top scores on beatmap `{}`...",
bm.short_link(Mods::NOMOD)
),
) )
.await?; .await?;
display_rankings_table(ctx, reply, scores, &bm, order).await?; display_rankings_table(ctx, reply, scores, show_diff, order).await?;
} }
ScoreListStyle::Grid => { ScoreListStyle::Grid => {
let reply = msg let reply = msg
.reply( .reply(
&ctx, &ctx,
format!( format!(
"Here are the top scores on beatmap `{}` of this server!", "Here are the top scores on {} of this server!",
bm.short_link(Mods::NOMOD) scoreboard_msg
), ),
) )
.await?; .await?;
style style
.display_scores( .display_scores(
scores.into_iter().map(|s| s.score).collect(), scores.into_iter().map(|s| s.score).collect(),
bm.1,
ctx, ctx,
Some(guild), Some(guild),
reply, reply,
@ -439,19 +441,30 @@ pub struct Ranking {
pub pp: f64, // calculated pp or score pp pub pp: f64, // calculated pp or score pp
pub official: bool, // official = pp is from bancho pub official: bool, // official = pp is from bancho
pub member: Arc<String>, pub member: Arc<String>,
pub beatmap: Arc<Beatmap>,
pub score: Score, pub score: Score,
pub star: f64,
} }
pub async fn get_leaderboard( async fn get_leaderboard(
ctx: &Context, ctx: &Context,
env: &OsuEnv, env: &OsuEnv,
bm: &BeatmapWithMode, beatmaps: impl IntoIterator<Item = Beatmap>,
mode_override: Option<Mode>,
show_unranked: bool, show_unranked: bool,
order: OrderBy, order: OrderBy,
guild: GuildId, guild: GuildId,
) -> Result<Vec<Ranking>> { ) -> Result<Vec<Ranking>> {
let BeatmapWithMode(beatmap, mode) = bm; let oppai_maps = beatmaps
let oppai_map = env.oppai.get_beatmap(beatmap.beatmap_id).await?; .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 let osu_users = env
.saved_users .saved_users
.all() .all()
@ -465,39 +478,48 @@ pub async fn get_leaderboard(
.query_members(&ctx, guild) .query_members(&ctx, guild)
.await? .await?
.iter() .iter()
.filter_map(|m| osu_users.get(&m.user.id).map(|ou| (m.distinct(), ou.id))) .filter_map(|m| {
.map(|(mem, osu_id)| { osu_users
env.client .get(&m.user.id)
.scores(bm.0.beatmap_id, move |f| { .map(|ou| (Arc::new(m.distinct()), ou.id))
f.user(UserID::ID(osu_id)).mode(bm.1) })
}) .flat_map(|(mem, osu_id)| {
.map(|r| Some((mem, r.ok()?))) 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<_>>() .collect::<FuturesUnordered<_>>()
.filter_map(future::ready) .filter_map(future::ready)
.collect::<Vec<_>>() .collect::<Vec<_>>()
.await .await
.into_iter() .into_iter()
.flat_map(|(mem, scores)| { .flat_map(|(b, op, mem, scores)| {
let mem = Arc::new(mem);
scores scores
.into_iter() .into_iter()
.map(|score| { .map(|score| {
let pp = score.pp.map(|v| (true, v)).unwrap_or_else(|| { let pp = score.pp.map(|v| (true, v)).unwrap_or_else(|| {
( (
false, false,
oppai_map.get_pp_from( op.get_pp_from(
*mode, mode_override.unwrap_or(b.mode),
Some(score.max_combo), Some(score.max_combo),
Stats::Raw(&score.statistics), Stats::Raw(&score.statistics),
&score.mods, &score.mods,
), ),
) )
}); });
let info = op.get_info_with(score.mode, &score.mods);
Ranking { Ranking {
pp: pp.1, pp: pp.1,
official: pp.0, official: pp.0,
beatmap: b.clone(),
member: mem.clone(), member: mem.clone(),
star: info.attrs.stars(),
score, score,
} }
}) })
@ -536,11 +558,55 @@ pub async fn get_leaderboard(
Ok(scores) 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( pub async fn display_rankings_table(
ctx: &Context, ctx: &Context,
to: Message, to: Message,
scores: Vec<Ranking>, scores: Vec<Ranking>,
bm: &BeatmapWithMode, show_diff: bool,
order: OrderBy, order: OrderBy,
) -> Result<()> { ) -> Result<()> {
let has_lazer_score = scores.iter().any(|v| v.score.mods.is_lazer); 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))); return Box::pin(future::ready(Ok(false)));
} }
let scores = scores[start..end].to_vec(); let scores = scores[start..end].to_vec();
let bm = (bm.0.clone(), bm.1);
let header = header.clone(); let header = header.clone();
Box::pin(async move { Box::pin(async move {
const SCORE_HEADERS: [&str; 8] = let headers: [&'static str; 9] = [
["#", "Score", "Mods", "Rank", "Acc", "Combo", "Miss", "User"]; "#",
const PP_HEADERS: [&str; 8] = match order {
["#", "PP", "Mods", "Rank", "Acc", "Combo", "Miss", "User"]; OrderBy::PP => "pp",
const ALIGNS: [Align; 8] = [Right, Right, Right, Right, Right, Right, Right, Left]; 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 let score_arr = scores
.iter() .iter()
@ -575,9 +660,11 @@ pub async fn display_rankings_table(
id, id,
Ranking { Ranking {
pp, pp,
beatmap,
official, official,
member, member,
score, 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(), score.rank.to_string(),
format!("{:.2}%", score.accuracy(bm.1)), format!("{:.2}%", score.accuracy(score.mode)),
format!("{}x", score.max_combo), format!("{}x", score.max_combo),
format!("{}", score.count_miss), format!("{}", score.count_miss),
time_before_now(&score.date),
member.to_string(), member.to_string(),
] ]
}, },
) )
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let score_table = match order { let score_table = table_formatting(&headers, &aligns, score_arr);
OrderBy::PP => table_formatting(&PP_HEADERS, &ALIGNS, score_arr),
OrderBy::Score => table_formatting(&SCORE_HEADERS, &ALIGNS, score_arr),
};
let content = MessageBuilder::new() let content = MessageBuilder::new()
.push_line(header.as_ref()) .push_line(header.as_ref())
.push_line(score_table) .push_line(score_table)
@ -617,15 +718,6 @@ pub async fn display_rankings_table(
page + 1, page + 1,
total_pages, 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(); .build();
m.edit(&ctx, EditMessage::new().content(content)).await?; 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 { pub fn mention(&self, override_mode: Option<Mode>, mods: &Mods) -> String {
format!( format!(
"[`{}`]({})", "[`{}`](<{}>)",
self.short_link(override_mode, mods), self.short_link(override_mode, mods),
self.mode_link(override_mode), self.mode_link(override_mode),
) )