From a36fa879645469dde741cb399e202834e9b6fedd Mon Sep 17 00:00:00 2001 From: Natsu Kagami Date: Mon, 10 Mar 2025 15:51:52 +0100 Subject: [PATCH] osu: Allow returning a full file instead when requesting a table check/leaderboard --- youmubot-osu/src/discord/commands.rs | 22 ++- youmubot-osu/src/discord/display.rs | 86 +++++--- youmubot-osu/src/discord/interaction.rs | 3 +- youmubot-osu/src/discord/server_rank.rs | 250 +++++++++++++----------- 4 files changed, 215 insertions(+), 146 deletions(-) diff --git a/youmubot-osu/src/discord/commands.rs b/youmubot-osu/src/discord/commands.rs index 22e8db9..a04cfe2 100644 --- a/youmubot-osu/src/discord/commands.rs +++ b/youmubot-osu/src/discord/commands.rs @@ -6,7 +6,7 @@ use display::display_beatmapset; use embeds::ScoreEmbedBuilder; use link_parser::EmbedType; use poise::{ChoiceParameter, CreateReply}; -use serenity::all::User; +use serenity::all::{CreateAttachment, User}; use server_rank::get_leaderboard_from_embed; /// osu!-related command group. @@ -581,6 +581,7 @@ async fn leaderboard( "Here are the top scores of **{}** on {}", guild.name, scoreboard_msg, ); + let has_lazer_score = scores.iter().any(|v| v.score.mods.is_lazer); match style { ScoreListStyle::Table => { @@ -589,11 +590,30 @@ async fn leaderboard( ctx.serenity_context(), reply, scores, + has_lazer_score, show_diff, sort.unwrap_or_default(), ) .await?; } + ScoreListStyle::File => { + ctx.send( + CreateReply::default() + .content(header) + .attachment(CreateAttachment::bytes( + server_rank::rankings_to_table( + &scores, + 0, + scores.len(), + has_lazer_score, + show_diff, + order, + ), + "rankings.txt", + )), + ) + .await?; + } ScoreListStyle::Grid => { let reply = ctx.reply(header).await?; style diff --git a/youmubot-osu/src/discord/display.rs b/youmubot-osu/src/discord/display.rs index 6a813c4..7918021 100644 --- a/youmubot-osu/src/discord/display.rs +++ b/youmubot-osu/src/discord/display.rs @@ -16,6 +16,8 @@ mod scores { Table, #[name = "List of Embeds"] Grid, + #[name = "Table File"] + File, } impl Default for ScoreListStyle { @@ -46,6 +48,7 @@ mod scores { ) -> Result<()> { match self { ScoreListStyle::Table => table::display_scores_table(scores, ctx, m).await, + ScoreListStyle::File => table::display_scores_as_file(scores, ctx, m).await, ScoreListStyle::Grid => grid::display_scores_grid(scores, ctx, guild_id, m).await, } } @@ -152,7 +155,7 @@ mod scores { use std::borrow::Cow; use pagination::paginate_with_first_message; - use serenity::all::CreateActionRow; + use serenity::all::{CreateActionRow, CreateAttachment}; use youmubot_prelude::table_format::Align::{Left, Right}; use youmubot_prelude::table_format::{table_formatting, Align}; @@ -162,6 +165,29 @@ mod scores { use crate::discord::{time_before_now, Beatmap, BeatmapInfo, OsuEnv}; use crate::models::Score; + pub async fn display_scores_as_file( + scores: Vec, + ctx: &Context, + mut on: impl CanEdit, + ) -> Result<()> { + if scores.is_empty() { + on.apply_edit(CreateReply::default().content("No plays found")) + .await?; + return Ok(()); + } + let p = Paginate { + env: ctx.data.read().await.get::().unwrap().clone(), + header: on.get_message().await?.content.clone(), + scores, + }; + let content = p.to_table(0, p.scores.len()).await; + on.apply_edit( + CreateReply::default().attachment(CreateAttachment::bytes(content, "table.txt")), + ) + .await?; + Ok(()) + } + pub async fn display_scores_table( scores: Vec, ctx: &Context, @@ -197,30 +223,13 @@ mod scores { fn total_pages(&self) -> usize { (self.scores.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE } - } - const ITEMS_PER_PAGE: usize = 5; + async fn to_table(&self, start: usize, end: usize) -> String { + let scores = &self.scores[start..end]; + let meta_cache = &self.env.beatmaps; + let oppai = &self.env.oppai; - #[async_trait] - impl pagination::Paginate for Paginate { - async fn render( - &mut self, - page: u8, - btns: Vec, - ) -> Result> { - let env = &self.env; - - let meta_cache = &env.beatmaps; - let oppai = &env.oppai; - let page = page as usize; - let start = page * ITEMS_PER_PAGE; - let end = self.scores.len().min(start + ITEMS_PER_PAGE); - if start >= end { - return Ok(None); - } - - let plays = &self.scores[start..end]; - let beatmaps = plays + let beatmaps = scores .iter() .map(|play| async move { let beatmap = meta_cache.get_beatmap(play.beatmap_id, play.mode).await?; @@ -234,7 +243,7 @@ mod scores { .map(|v| v.ok()) .collect::>(); - let pps = plays + let pps = scores .iter() .map(|p| async move { match p.pp.map(|pp| format!("{:.2}", pp)) { @@ -257,7 +266,7 @@ mod scores { let (beatmaps, pps) = future::join(beatmaps, pps).await; - let ranks = plays + let ranks = scores .iter() .enumerate() .map(|(i, p)| -> Cow<'static, str> { @@ -288,7 +297,7 @@ mod scores { .into_iter() .enumerate() .map(|(i, b)| { - let play = &plays[i]; + let play = &scores[i]; b.map(|(beatmap, info)| { format!( "[{:.1}*] {} - {} [{}] ({})", @@ -307,7 +316,7 @@ mod scores { ["#", "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 = scores .iter() .zip(beatmaps.iter()) .zip(ranks.iter().zip(pps.iter())) @@ -325,9 +334,30 @@ mod scores { }) .collect::>(); - let score_table = table_formatting(&SCORE_HEADERS, &SCORE_ALIGNS, score_arr); + table_formatting(&SCORE_HEADERS, &SCORE_ALIGNS, score_arr) + } + } + + const ITEMS_PER_PAGE: usize = 5; + + #[async_trait] + impl pagination::Paginate for Paginate { + async fn render( + &mut self, + page: u8, + btns: Vec, + ) -> Result> { + let page = page as usize; + let start = page * ITEMS_PER_PAGE; + let end = self.scores.len().min(start + ITEMS_PER_PAGE); + if start >= end { + return Ok(None); + } + let plays = &self.scores[start..end]; + let has_oppai = plays.iter().any(|p| p.pp.is_none()); + let score_table = self.to_table(start, end).await; let mut content = serenity::utils::MessageBuilder::new(); content .push_line(&self.header) diff --git a/youmubot-osu/src/discord/interaction.rs b/youmubot-osu/src/discord/interaction.rs index 115c9b0..253c946 100644 --- a/youmubot-osu/src/discord/interaction.rs +++ b/youmubot-osu/src/discord/interaction.rs @@ -413,7 +413,8 @@ pub fn handle_lb_button<'a>( .content(format!("Here are the top scores on {}!", scoreboard_msg)), ) .await?; - display_rankings_table(ctx, reply, scores, show_diff, order).await?; + let has_lazer_score = scores.iter().any(|s| s.score.mods.is_lazer); + display_rankings_table(ctx, reply, scores, has_lazer_score, show_diff, order).await?; Ok(()) }) } diff --git a/youmubot-osu/src/discord/server_rank.rs b/youmubot-osu/src/discord/server_rank.rs index e6b7af1..7d7d092 100644 --- a/youmubot-osu/src/discord/server_rank.rs +++ b/youmubot-osu/src/discord/server_rank.rs @@ -10,7 +10,7 @@ use std::{ use chrono::DateTime; use pagination::paginate_with_first_message; use serenity::{ - all::{GuildId, Member, PartialGuild}, + all::{CreateAttachment, CreateMessage, GuildId, Member, PartialGuild}, framework::standard::{macros::command, Args, CommandResult}, model::channel::Message, utils::MessageBuilder, @@ -401,26 +401,41 @@ pub async fn show_leaderboard(ctx: &Context, msg: &Message, mut args: Args) -> C return Ok(()); } + let header = format!( + "Here are the top scores of **{}** on {}", + guild.name(&ctx).unwrap(), + scoreboard_msg, + ); + let has_lazer_score = scores.iter().any(|v| v.score.mods.is_lazer); + match style { ScoreListStyle::Table => { - let reply = msg - .reply( + let reply = msg.reply(&ctx, header).await?; + display_rankings_table(ctx, reply, scores, has_lazer_score, show_diff, order).await?; + } + ScoreListStyle::File => { + msg.channel_id + .send_message( &ctx, - format!("⌛ Loading top scores on {}...", scoreboard_msg), + CreateMessage::new() + .content(header) + .reference_message(msg) + .add_file(CreateAttachment::bytes( + rankings_to_table( + &scores, + 0, + scores.len(), + has_lazer_score, + show_diff, + order, + ), + "rankings.txt", + )), ) .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 {} of this server!", - scoreboard_msg - ), - ) - .await?; + let reply = msg.reply(&ctx, header).await?; style .display_scores( scores.into_iter().map(|s| s.score).collect(), @@ -600,19 +615,107 @@ pub async fn get_leaderboard_from_embed( }) } +pub(crate) fn rankings_to_table( + scores: &[Ranking], + start: usize, + end: usize, + has_lazer_score: bool, + show_diff: bool, + order: OrderBy, +) -> String { + assert!(start < end); + 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() + .enumerate() + .map( + |( + id, + Ranking { + pp, + beatmap, + official, + member, + score, + star, + }, + )| { + [ + format!("{}", 1 + id + start), + match order { + OrderBy::PP => { + format!("{:.2}{}", pp, if *official { "" } else { "[?]" }) + } + OrderBy::Score => { + crate::discord::embeds::grouped_number(if has_lazer_score { + score.normalized_score as u64 + } else { + score.score + }) + } + }, + 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(score.mode)), + format!("{}x", score.max_combo), + format!("{}", score.count_miss), + time_before_now(&score.date), + member.to_string(), + ] + }, + ) + .collect::>(); + table_formatting(&headers, &aligns, &score_arr) +} + pub async fn display_rankings_table( ctx: &Context, to: Message, scores: Vec, + has_lazer_score: bool, show_diff: bool, order: OrderBy, ) -> Result<()> { - let has_lazer_score = scores.iter().any(|v| v.score.mods.is_lazer); - const ITEMS_PER_PAGE: usize = 5; let total_len = scores.len(); let total_pages = (total_len + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE; - let header = Arc::new(to.content.clone()); + let header = to.content.clone(); paginate_with_first_message( paginate_from_fn(move |page: u8, btns| { @@ -622,106 +725,21 @@ pub async fn display_rankings_table( return Box::pin(future::ready(Ok(None))); } let scores = scores[start..end].to_vec(); - let header = header.clone(); - Box::pin(async move { - 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() - .enumerate() - .map( - |( - id, - Ranking { - pp, - beatmap, - official, - member, - score, - star, - }, - )| { - [ - format!("{}", 1 + id + start), - match order { - OrderBy::PP => { - format!("{:.2}{}", pp, if *official { "" } else { "[?]" }) - } - OrderBy::Score => { - crate::discord::embeds::grouped_number(if has_lazer_score { - score.normalized_score as u64 - } else { - score.score - }) - } - }, - 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(score.mode)), - format!("{}x", score.max_combo), - format!("{}", score.count_miss), - time_before_now(&score.date), - member.to_string(), - ] - }, - ) - .collect::>(); - - let score_table = table_formatting(&headers, &aligns, score_arr); - let content = MessageBuilder::new() - .push_line(header.as_ref()) - .push_line(score_table) - .push_line(format!( - "Page **{}**/**{}**. Not seeing your scores? Run `osu check` to update.", - page + 1, - total_pages, - )) - .build(); - - Ok(Some( - CreateReply::default().content(content).components(btns), + let score_table = + rankings_to_table(&scores, start, end, has_lazer_score, show_diff, order); + let content = MessageBuilder::new() + .push_line(&header) + .push_line(score_table) + .push_line(format!( + "Page **{}**/**{}**. Not seeing your scores? Run `osu check` to update.", + page + 1, + total_pages, )) - }) + .build(); + + Box::pin(future::ready(Ok(Some( + CreateReply::default().content(content).components(btns), + )))) }) .with_page_count(total_pages), ctx,