Implement recent

This commit is contained in:
Natsu Kagami 2020-01-13 17:28:51 -05:00
parent 4020405801
commit 3af0b56755
4 changed files with 235 additions and 8 deletions

View file

@ -17,9 +17,13 @@ impl<'de> Deserialize<'de> for Score {
user_id: parse_from_str(&raw.user_id)?, user_id: parse_from_str(&raw.user_id)?,
date: parse_date(&raw.date)?, date: parse_date(&raw.date)?,
beatmap_id: raw.beatmap_id.map(parse_from_str).transpose()?.unwrap_or(0), beatmap_id: raw.beatmap_id.map(parse_from_str).transpose()?.unwrap_or(0),
replay_available: parse_bool(&raw.replay_available)?, replay_available: raw
.replay_available
.map(parse_bool)
.transpose()?
.unwrap_or(false),
score: parse_from_str(&raw.score)?, score: parse_from_str(&raw.score)?,
pp: parse_from_str(&raw.pp)?, pp: raw.pp.map(parse_from_str).transpose()?,
rank: parse_from_str(&raw.rank)?, rank: parse_from_str(&raw.rank)?,
mods: { mods: {
let v: u64 = parse_from_str(&raw.enabled_mods)?; let v: u64 = parse_from_str(&raw.enabled_mods)?;

View file

@ -159,6 +159,14 @@ impl Beatmap {
self.beatmapset_id, NEW_MODE_NAMES[self.mode as usize], self.beatmap_id self.beatmapset_id, NEW_MODE_NAMES[self.mode as usize], self.beatmap_id
) )
} }
/// Link to the cover image of the beatmap.
pub fn cover_url(&self) -> String {
format!(
"https://assets.ppy.sh/beatmaps/{}/covers/cover.jpg",
self.beatmapset_id
)
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -198,6 +206,16 @@ pub struct User {
pub accuracy: f64, pub accuracy: f64,
} }
impl User {
pub fn link(&self) -> String {
format!("https://osu.ppy.sh/users/{}", self.id)
}
pub fn avatar_url(&self) -> String {
format!("https://a.ppy.sh/{}", self.id)
}
}
#[derive(Debug)] #[derive(Debug)]
pub enum Rank { pub enum Rank {
SS, SS,
@ -244,7 +262,7 @@ pub struct Score {
pub beatmap_id: u64, pub beatmap_id: u64,
pub score: u64, pub score: u64,
pub pp: f64, pub pp: Option<f64>,
pub rank: Rank, pub rank: Rank,
pub mods: Mods, // Later pub mods: Mods, // Later
@ -257,3 +275,41 @@ pub struct Score {
pub max_combo: u64, pub max_combo: u64,
pub perfect: bool, pub perfect: bool,
} }
impl Score {
/// Given the play's mode, calculate the score's accuracy.
pub fn accuracy(&self, mode: Mode) -> f64 {
100.0
* match mode {
Mode::Std => {
(6 * self.count_300 + 2 * self.count_100 + self.count_50) as f64
/ (6.0
* (self.count_300 + self.count_100 + self.count_50 + self.count_miss)
as f64)
}
Mode::Taiko => {
(2 * self.count_300 + self.count_100) as f64
/ 2.0
/ (self.count_300 + self.count_100 + self.count_miss) as f64
}
Mode::Catch => {
(self.count_300 + self.count_100) as f64
/ (self.count_300 + self.count_100 + self.count_miss + self.count_katu/* # of droplet misses */)
as f64
}
Mode::Mania => {
((self.count_geki /* MAX */ + self.count_300) * 6
+ self.count_katu /* 200 */ * 4
+ self.count_100 * 2
+ self.count_50) as f64
/ 6.0
/ (self.count_geki
+ self.count_300
+ self.count_katu
+ self.count_100
+ self.count_50
+ self.count_miss) as f64
}
}
}
}

View file

@ -93,6 +93,6 @@ pub(crate) struct Score {
pub user_id: String, pub user_id: String,
pub date: String, pub date: String,
pub rank: String, pub rank: String,
pub pp: String, pub pp: Option<String>,
pub replay_available: String, pub replay_available: Option<String>,
} }

View file

@ -13,13 +13,15 @@ use serenity::{
utils::MessageBuilder, utils::MessageBuilder,
}; };
use youmubot_osu::{ use youmubot_osu::{
models::{Beatmap, Mode, Score, User}, models::{Beatmap, Mode, Rank, Score, User},
request::{BeatmapRequestKind, UserID}, request::{BeatmapRequestKind, UserID},
Client as OsuClient,
}; };
mod hook; mod hook;
pub use hook::hook; pub use hook::hook;
use std::str::FromStr;
group!({ group!({
name: "osu", name: "osu",
@ -27,7 +29,7 @@ group!({
prefix: "osu", prefix: "osu",
description: "osu! related commands.", description: "osu! related commands.",
}, },
commands: [std, taiko, catch, mania, save], commands: [std, taiko, catch, mania, save, recent],
}); });
#[command] #[command]
@ -101,6 +103,96 @@ pub fn save(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
Ok(()) Ok(())
} }
struct ModeArg(Mode);
impl FromStr for ModeArg {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(ModeArg(match s {
"std" => Mode::Std,
"taiko" => Mode::Taiko,
"catch" => Mode::Catch,
"mania" => Mode::Mania,
_ => return Err(Error::from(format!("Unknown mode {}", s))),
}))
}
}
#[command]
#[description = "Gets an user's recent play"]
#[usage = "#[the nth recent play = 1] / [mode (std, taiko, mania, catch) = std] / [username / user id = your saved id]"]
#[example = "#1 / taiko / natsukagami"]
#[max_args(3)]
pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
struct Nth(u8);
impl FromStr for Nth {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if !s.starts_with("#") {
Err(Error::from("Not an order"))
} else {
let v = s.split_at("#".len()).1.parse()?;
Ok(Nth(v))
}
}
}
let mut data = ctx.data.write();
let nth = args.single::<Nth>().unwrap_or(Nth(1)).0.min(50).max(1);
let mode = args.single::<ModeArg>().unwrap_or(ModeArg(Mode::Std)).0;
let user = match args.single::<String>() {
Ok(v) => v,
Err(_) => {
let db: DBWriteGuard<_> = data
.get_mut::<OsuSavedUsers>()
.ok_or(Error::from("DB uninitialized"))?
.into();
let db = db.borrow()?;
match db.get(&msg.author.id) {
Some(ref v) => v.to_string(),
None => {
msg.reply(&ctx, "You have not saved any account.")?;
return Ok(());
}
}
}
};
dbg!((nth, &mode, &user, &args));
let reqwest = data.get::<http::HTTP>().unwrap();
let osu: &OsuClient = data.get::<http::Osu>().unwrap();
let user = osu
.user(reqwest, UserID::Auto(user), |f| f.mode(mode))?
.ok_or(Error::from("User not found"))?;
let recent_play = osu
.user_recent(reqwest, UserID::ID(user.id), |f| f.mode(mode).limit(nth))?
.into_iter()
.last()
.ok_or(Error::from("No such play"))?;
let beatmap = osu
.beatmaps(
reqwest,
BeatmapRequestKind::Beatmap(recent_play.beatmap_id),
|f| f.mode(mode, true),
)?
.into_iter()
.next()
.unwrap();
msg.channel_id.send_message(&ctx, |m| {
m.content(format!(
"{}: here is the play that you requested",
msg.author
))
.embed(|m| score_embed(&recent_play, &beatmap, &user, &mode, None, m))
})?;
Ok(())
}
fn get_user(ctx: &mut Context, msg: &Message, args: Args, mode: Mode) -> CommandResult { fn get_user(ctx: &mut Context, msg: &Message, args: Args, mode: Mode) -> CommandResult {
let mut data = ctx.data.write(); let mut data = ctx.data.write();
let username = match args.remains() { let username = match args.remains() {
@ -147,6 +239,78 @@ fn get_user(ctx: &mut Context, msg: &Message, args: Args, mode: Mode) -> Command
Ok(()) Ok(())
} }
fn score_embed<'a>(
s: &Score,
b: &Beatmap,
u: &User,
mode: &Mode,
top_record: Option<u8>,
m: &'a mut CreateEmbed,
) -> &'a mut CreateEmbed {
let accuracy = s.accuracy(*mode);
let score_line = match &s.rank {
Rank::SS | Rank::SSH => format!("SS"),
_ if s.perfect => format!("{:2}% FC", accuracy),
Rank::F => format!("{:.2}% {} combo [FAILED]", accuracy, s.max_combo),
v => format!("{:.2}% {} combo {} rank", accuracy, s.max_combo, v),
};
let score_line =
s.pp.map(|pp| format!("{} | {:2}pp", &score_line, pp))
.unwrap_or(score_line);
let top_record = top_record
.map(|v| format!("| #{} top record!", v))
.unwrap_or("".to_owned());
m.author(|f| f.name(&u.username).url(u.link()).icon_url(u.avatar_url()))
.color(0xffb6c1)
.title(format!(
"{} | {} - {} [{}] {} ({:.2}\\*) by {} | {} {}",
u.username,
b.artist,
b.title,
b.difficulty_name,
s.mods,
b.difficulty.stars,
b.creator,
score_line,
top_record
))
.description(format!("[[Beatmap]]({})", b.link()))
.image(b.cover_url())
.field(
"Beatmap",
format!("{} - {} [{}]", b.artist, b.title, b.difficulty_name),
false,
)
.field("Rank", &score_line, false)
.fields(s.pp.map(|pp| ("pp gained", format!("{:2}pp", pp), true)))
.field("Creator", &b.creator, true)
.field("Mode", mode.to_string(), true)
.field(
"Map stats",
MessageBuilder::new()
.push(format!("[[Link]]({})", b.link()))
.push(", ")
.push_bold(format!("{:.2}", b.difficulty.stars))
.push(", ")
.push_bold_line(
b.mode.to_string() + if b.mode == *mode { "" } else { " (Converted)" },
)
.push("CS")
.push_bold(format!("{:.1}", b.difficulty.cs))
.push(", AR")
.push_bold(format!("{:.1}", b.difficulty.ar))
.push(", OD")
.push_bold(format!("{:.1}", b.difficulty.od))
.push(", HP")
.push_bold(format!("{:.1}", b.difficulty.hp))
.push(", ⌛ ")
.push_bold(format!("{}", Duration(b.drain_length)))
.build(),
false,
)
.field("Played on", s.date.format("%F %T"), false)
}
fn user_embed<'a>( fn user_embed<'a>(
u: User, u: User,
best: Option<(Score, Beatmap)>, best: Option<(Score, Beatmap)>,
@ -195,7 +359,10 @@ fn user_embed<'a>(
( (
"Best Record", "Best Record",
MessageBuilder::new() MessageBuilder::new()
.push_bold(format!("{:.2}pp", v.pp)) .push_bold(format!(
"{:.2}pp",
v.pp.unwrap() /*Top record should have pp*/
))
.push(" - ") .push(" - ")
.push_line(format!("{:.1} ago", Duration(Utc::now() - v.date))) .push_line(format!("{:.1} ago", Duration(Utc::now() - v.date)))
.push("on ") .push("on ")