osu: Allow returning a full file instead when requesting a table check/leaderboard

This commit is contained in:
Natsu Kagami 2025-03-10 15:51:52 +01:00
parent ed15406f51
commit a36fa87964
Signed by: nki
GPG key ID: 55A032EB38B49ADB
4 changed files with 215 additions and 146 deletions

View file

@ -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<U: HasOsuEnv>(
"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<U: HasOsuEnv>(
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

View file

@ -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<Score>,
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::<OsuEnv>().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<Score>,
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<CreateActionRow>,
) -> Result<Option<CreateReply>> {
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::<Vec<_>>();
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::<Vec<_>>();
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<CreateActionRow>,
) -> Result<Option<CreateReply>> {
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)

View file

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

View file

@ -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::<Vec<_>>();
table_formatting(&headers, &aligns, &score_arr)
}
pub async fn display_rankings_table(
ctx: &Context,
to: Message,
scores: Vec<Ranking>,
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::<Vec<_>>();
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,