Use grid throughout the commands

This commit is contained in:
Natsu Kagami 2021-04-28 14:00:53 +09:00
parent ba8b835cc2
commit f27939546e
Signed by: nki
GPG key ID: 7306B3D3C3AD6E51
4 changed files with 188 additions and 60 deletions

View file

@ -1,7 +1,126 @@
pub use beatmapset::display_beatmapset;
pub use scores::table::display_scores_table;
pub use scores::ScoreListStyle;
mod scores {
use crate::models::{Mode, Score};
use serenity::{framework::standard::CommandResult, model::channel::Message};
use youmubot_prelude::*;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
/// The style for the scores list to be displayed.
pub enum ScoreListStyle {
Table,
Grid,
}
impl Default for ScoreListStyle {
fn default() -> Self {
Self::Table
}
}
impl std::str::FromStr for ScoreListStyle {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"--table" => Ok(Self::Table),
"--grid" => Ok(Self::Grid),
_ => Err(Error::msg("unknown value")),
}
}
}
impl ScoreListStyle {
pub async fn display_scores<'a>(
self,
scores: Vec<Score>,
mode: Mode,
ctx: &'a Context,
m: &'a Message,
) -> CommandResult {
match self {
ScoreListStyle::Table => table::display_scores_table(scores, mode, ctx, m).await,
ScoreListStyle::Grid => grid::display_scores_grid(scores, mode, ctx, m).await,
}
}
}
pub mod grid {
use crate::discord::{
cache::save_beatmap, BeatmapCache, BeatmapMetaCache, BeatmapWithMode,
};
use crate::models::{Mode, Score};
use serenity::{framework::standard::CommandResult, model::channel::Message};
use youmubot_prelude::*;
pub async fn display_scores_grid<'a>(
scores: Vec<Score>,
mode: Mode,
ctx: &'a Context,
m: &'a Message,
) -> CommandResult {
if scores.is_empty() {
m.reply(&ctx, "No plays found").await?;
return Ok(());
}
paginate_reply(
Paginate { scores, mode },
ctx,
m,
std::time::Duration::from_secs(60),
)
.await?;
Ok(())
}
pub struct Paginate {
scores: Vec<Score>,
mode: Mode,
}
#[async_trait]
impl pagination::Paginate for Paginate {
async fn render(&mut self, page: u8, ctx: &Context, msg: &mut Message) -> Result<bool> {
let data = ctx.data.read().await;
let client = data.get::<crate::discord::OsuClient>().unwrap();
let osu = data.get::<BeatmapMetaCache>().unwrap();
let beatmap_cache = data.get::<BeatmapCache>().unwrap();
let page = page as usize;
let score = &self.scores[page];
let hourglass = msg.react(ctx, '⌛').await?;
let mode = self.mode;
let beatmap = osu.get_beatmap(score.beatmap_id, mode).await?;
let content = beatmap_cache.get_beatmap(beatmap.beatmap_id).await?;
let bm = BeatmapWithMode(beatmap, mode);
let user = client
.user(crate::request::UserID::ID(score.user_id), |f| f)
.await?
.ok_or_else(|| Error::msg("user not found"))?;
msg.edit(ctx, |e| {
e.embed(|e| {
crate::discord::embeds::score_embed(score, &bm, &content, &user)
.footer(format!("Page {}/{}", page + 1, self.scores.len()))
.build(e)
})
})
.await?;
save_beatmap(&*ctx.data.read().await, msg.channel_id, &bm).await?;
// End
hourglass.delete(ctx).await?;
Ok(true)
}
fn len(&self) -> Option<usize> {
Some(self.scores.len())
}
}
}
pub mod table {
use crate::discord::{Beatmap, BeatmapCache, BeatmapInfo, BeatmapMetaCache};
use crate::models::{Mode, Score};

View file

@ -169,6 +169,7 @@ pub(crate) struct ScoreEmbedBuilder<'a> {
u: &'a User,
top_record: Option<u8>,
world_record: Option<u16>,
footer: Option<String>,
}
impl<'a> ScoreEmbedBuilder<'a> {
@ -180,6 +181,10 @@ impl<'a> ScoreEmbedBuilder<'a> {
self.world_record = Some(rank);
self
}
pub fn footer(&mut self, footer: impl Into<String>) -> &mut Self {
self.footer = Some(footer.into());
self
}
}
pub(crate) fn score_embed<'a>(
@ -195,12 +200,13 @@ pub(crate) fn score_embed<'a>(
u,
top_record: None,
world_record: None,
footer: None,
}
}
impl<'a> ScoreEmbedBuilder<'a> {
#[allow(clippy::many_single_char_names)]
pub fn build<'b>(&self, m: &'b mut CreateEmbed) -> &'b mut CreateEmbed {
pub fn build<'b>(&mut self, m: &'b mut CreateEmbed) -> &'b mut CreateEmbed {
let mode = self.bm.mode();
let b = &self.bm.0;
let s = self.s;
@ -358,8 +364,12 @@ impl<'a> ScoreEmbedBuilder<'a> {
)
.field("Map stats", diff.format_info(mode, s.mods, b), false)
.timestamp(&s.date);
let mut footer = self.footer.take().unwrap_or_else(String::new);
if mode.to_oppai_mode().is_none() && s.mods != Mods::NOMOD {
m.footer(|f| f.text("Star difficulty does not reflect game mods."));
footer += " Star difficulty does not reflect game mods.";
}
if !footer.is_empty() {
m.footer(|f| f.text(footer));
}
m
}

View file

@ -1,6 +1,6 @@
use crate::{
discord::beatmap_cache::BeatmapMetaCache,
discord::display::display_scores_table as list_scores,
discord::display::ScoreListStyle,
discord::oppai_cache::{BeatmapCache, BeatmapInfo},
models::{Beatmap, Mode, Mods, User},
request::UserID,
@ -32,7 +32,7 @@ use db::OsuUser;
use db::{OsuLastBeatmap, OsuSavedUsers, OsuUserBests};
use embeds::{beatmap_embed, score_embed, user_embed};
pub use hook::hook;
use server_rank::{LEADERBOARD_COMMAND, SERVER_RANK_COMMAND, UPDATE_LEADERBOARD_COMMAND};
use server_rank::{SERVER_RANK_COMMAND, UPDATE_LEADERBOARD_COMMAND};
/// The osu! client.
pub(crate) struct OsuClient;
@ -108,7 +108,6 @@ pub fn setup(
check,
top,
server_rank,
leaderboard,
update_leaderboard
)]
#[default_command(std)]
@ -315,13 +314,15 @@ impl FromStr for Nth {
}
#[command]
#[aliases("rs", "rc")]
#[description = "Gets an user's recent play"]
#[usage = "#[the nth recent play = --all] / [mode (std, taiko, mania, catch) = std] / [username / user id = your saved id]"]
#[usage = "#[the nth recent play = --all] / [style (table or grid) = --table] / [mode (std, taiko, mania, catch) = std] / [username / user id = your saved id]"]
#[example = "#1 / taiko / natsukagami"]
#[max_args(3)]
#[max_args(4)]
pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await;
let nth = args.single::<Nth>().unwrap_or(Nth::All);
let style = args.single::<ScoreListStyle>().unwrap_or_default();
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*data, msg).await?;
@ -361,7 +362,7 @@ pub async fn recent(ctx: &Context, msg: &Message, mut args: Args) -> CommandResu
let plays = osu
.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))
.await?;
list_scores(plays, mode, ctx, msg).await?;
style.display_scores(plays, mode, ctx, msg).await?;
}
}
Ok(())
@ -433,9 +434,9 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
#[command]
#[aliases("c", "chk")]
#[usage = "[username or tag = yourself]"]
#[usage = "[style (table or grid) = --table] / [username or tag = yourself]"]
#[description = "Check your own or someone else's best record on the last beatmap. Also stores the result if possible."]
#[max_args(1)]
#[max_args(2)]
pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await;
let bm = cache::get_beatmap(&*data, msg.channel_id).await?;
@ -448,6 +449,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
Some(bm) => {
let b = &bm.0;
let m = bm.1;
let style = args.single::<ScoreListStyle>().unwrap_or_default();
let username_arg = args.single::<UsernameArg>().ok();
let user_id = match username_arg.as_ref() {
Some(UsernameArg::Tagged(v)) => Some(*v),
@ -457,9 +459,6 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
let user = to_user_id_query(username_arg, &*data, msg).await?;
let osu = data.get::<OsuClient>().unwrap();
let oppai = data.get::<BeatmapCache>().unwrap();
let content = oppai.get_beatmap(b.beatmap_id).await?;
let user = osu
.user(user, |f| f)
@ -473,22 +472,16 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
msg.reply(&ctx, "No scores found").await?;
}
for score in scores.iter() {
msg.channel_id
.send_message(&ctx, |c| {
c.embed(|m| score_embed(&score, &bm, &content, &user).build(m))
})
.await?;
}
if let Some(user_id) = user_id {
// Save to database
data.get::<OsuUserBests>()
.unwrap()
.save(user_id, m, scores)
.save(user_id, m, scores.clone())
.await
.pls_ok();
}
style.display_scores(scores, m, ctx, msg).await?;
}
}
@ -497,12 +490,13 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
#[command]
#[description = "Get the n-th top record of an user."]
#[usage = "[mode (std, taiko, catch, mania)] = std / #[n-th = --all] / [username or user_id = your saved user id]"]
#[example = "taiko / #2 / natsukagami"]
#[max_args(3)]
#[usage = "#[n-th = --all] / [style (table or grid) = --table] / [mode (std, taiko, catch, mania)] = std / [username or user_id = your saved user id]"]
#[example = "#2 / taiko / natsukagami"]
#[max_args(4)]
pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult {
let data = ctx.data.read().await;
let nth = args.single::<Nth>().unwrap_or(Nth::All);
let style = args.single::<ScoreListStyle>().unwrap_or_default();
let mode = args
.single::<ModeArg>()
.map(|ModeArg(t)| t)
@ -555,7 +549,7 @@ pub async fn top(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
let plays = osu
.user_best(UserID::ID(user.id), |f| f.mode(mode).limit(100))
.await?;
list_scores(plays, mode, ctx, msg).await?;
style.display_scores(plays, mode, ctx, msg).await?;
}
}
Ok(())

View file

@ -5,6 +5,7 @@ use super::{
};
use crate::{
discord::{
display::ScoreListStyle,
oppai_cache::{BeatmapCache, OppaiAccuracy},
BeatmapWithMode,
},
@ -151,23 +152,33 @@ enum OrderBy {
Score,
}
impl From<&str> for OrderBy {
fn from(s: &str) -> Self {
if s == "--score" {
impl Default for OrderBy {
fn default() -> Self {
Self::Score
} else {
Self::PP
}
}
impl std::str::FromStr for OrderBy {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"--score" => Ok(OrderBy::Score),
"--pp" => Ok(OrderBy::PP),
_ => Err(Error::msg("unknown value")),
}
}
}
#[command("updatelb")]
#[description = "Update the leaderboard on the last seen beatmap"]
#[usage = "[--score to sort by score, default to sort by pp]"]
#[max_args(1)]
#[command("leaderboard")]
#[aliases("lb", "bmranks", "br", "cc", "updatelb")]
#[usage = "[--score to sort by score, default to sort by pp] / [--table to show a table, --grid to show score by score]"]
#[description = "See the server's ranks on the last seen beatmap"]
#[max_args(2)]
#[only_in(guilds)]
pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> CommandResult {
let sort_order = OrderBy::from(args.rest());
pub async fn update_leaderboard(ctx: &Context, m: &Message, mut args: Args) -> CommandResult {
let sort_order = args.single::<OrderBy>().unwrap_or_default();
let style = args.single::<ScoreListStyle>().unwrap_or_default();
let guild = m.guild_id.unwrap();
let data = ctx.data.read().await;
@ -246,27 +257,7 @@ pub async fn update_leaderboard(ctx: &Context, m: &Message, args: Args) -> Comma
.await
.ok();
drop(update_lock);
show_leaderboard(ctx, m, bm, sort_order).await
}
#[command("leaderboard")]
#[aliases("lb", "bmranks", "br", "cc")]
#[usage = "[--score to sort by score, default to sort by pp]"]
#[description = "See the server's ranks on the last seen beatmap"]
#[max_args(1)]
#[only_in(guilds)]
pub async fn leaderboard(ctx: &Context, m: &Message, args: Args) -> CommandResult {
let sort_order = OrderBy::from(args.rest());
let data = ctx.data.read().await;
let bm = match get_beatmap(&*data, m.channel_id).await? {
Some(bm) => bm,
None => {
m.reply(&ctx, "No beatmap queried on this channel.").await?;
return Ok(());
}
};
show_leaderboard(ctx, m, bm, sort_order).await
show_leaderboard(ctx, m, bm, sort_order, style).await
}
async fn show_leaderboard(
@ -274,6 +265,7 @@ async fn show_leaderboard(
m: &Message,
bm: BeatmapWithMode,
order: OrderBy,
style: ScoreListStyle,
) -> CommandResult {
let data = ctx.data.read().await;
@ -381,6 +373,19 @@ async fn show_leaderboard(
.await?;
return Ok(());
}
if let ScoreListStyle::Grid = style {
style
.display_scores(
scores.into_iter().map(|(_, _, a)| a).collect(),
mode,
ctx,
m,
)
.await?;
return Ok(());
}
paginate_reply_fn(
move |page: u8, ctx: &Context, m: &mut Message| {
const ITEMS_PER_PAGE: usize = 5;