Hooks and last works

This commit is contained in:
Natsu Kagami 2020-06-12 02:44:56 -04:00
parent f8cbd7ceb0
commit c43f9067b8
Signed by: nki
GPG key ID: 73376E117CD20735
5 changed files with 135 additions and 34 deletions

View file

@ -1,5 +1,8 @@
use super::BeatmapWithMode; use super::BeatmapWithMode;
use crate::models::{Beatmap, Mode, Rank, Score, User}; use crate::{
discord::oppai_cache::BeatmapInfo,
models::{Beatmap, Mode, Mods, Rank, Score, User},
};
use chrono::Utc; use chrono::Utc;
use serenity::{builder::CreateEmbed, utils::MessageBuilder}; use serenity::{builder::CreateEmbed, utils::MessageBuilder};
use youmubot_prelude::*; use youmubot_prelude::*;
@ -12,7 +15,32 @@ fn format_mode(actual: Mode, original: Mode) -> String {
} }
} }
pub fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a mut CreateEmbed { pub fn beatmap_embed<'a>(
b: &'_ Beatmap,
m: Mode,
mods: Mods,
info: Option<BeatmapInfo>,
c: &'a mut CreateEmbed,
) -> &'a mut CreateEmbed {
let mod_str = if mods == Mods::NOMOD {
"".to_owned()
} else {
format!(" {}", mods)
};
let total_length = if mods.intersects(Mods::DT | Mods::NC) {
b.total_length * 2 / 3
} else if mods.intersects(Mods::HT) {
b.total_length * 4 / 3
} else {
b.total_length
};
let drain_length = if mods.intersects(Mods::DT | Mods::NC) {
b.drain_length * 2 / 3
} else if mods.intersects(Mods::HT) {
b.drain_length * 4 / 3
} else {
b.drain_length
};
c.title( c.title(
MessageBuilder::new() MessageBuilder::new()
.push_bold_safe(&b.artist) .push_bold_safe(&b.artist)
@ -21,6 +49,7 @@ pub fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a
.push(" [") .push(" [")
.push_bold_safe(&b.difficulty_name) .push_bold_safe(&b.difficulty_name)
.push("]") .push("]")
.push(&mod_str)
.build(), .build(),
) )
.author(|a| { .author(|a| {
@ -34,15 +63,29 @@ pub fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a
.color(0xffb6c1) .color(0xffb6c1)
.field( .field(
"Star Difficulty", "Star Difficulty",
format!("{:.2}", b.difficulty.stars), format!(
"{:.2}⭐",
info.map(|v| v.stars as f64).unwrap_or(b.difficulty.stars)
),
false, false,
) )
.fields(info.map(|info| {
(
"Calculated pp",
format!(
"95%: **{:.2}**pp, 98%: **{:.2}**pp, 99%: **{:.2}**pp, 100%: **{:.2}**pp",
info.pp[0], info.pp[1], info.pp[2], info.pp[3]
),
false,
)
}))
.fields(Some(("Mods", mods, false)).filter(|_| mods != Mods::NOMOD))
.field( .field(
"Length", "Length",
MessageBuilder::new() MessageBuilder::new()
.push_bold_safe(Duration(b.total_length)) .push_bold_safe(Duration(total_length))
.push(" (") .push(" (")
.push_bold_safe(Duration(b.drain_length)) .push_bold_safe(Duration(drain_length))
.push(" drain)") .push(" drain)")
.build(), .build(),
false, false,
@ -90,6 +133,12 @@ pub fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a
.push_bold(&b.genre) .push_bold(&b.genre)
.build(), .build(),
) )
.footer(|f| {
if info.is_none() && mods != Mods::NOMOD {
f.text("Star difficulty not reflecting mods applied.");
}
f
})
} }
const MAX_DIFFS: usize = 25 - 4; const MAX_DIFFS: usize = 25 - 4;

View file

@ -1,6 +1,7 @@
use super::OsuClient; use super::OsuClient;
use crate::{ use crate::{
models::{Beatmap, Mode}, discord::oppai_cache::{BeatmapCache, BeatmapInfo},
models::{Beatmap, Mode, Mods},
request::BeatmapRequestKind, request::BeatmapRequestKind,
}; };
use lazy_static::lazy_static; use lazy_static::lazy_static;
@ -11,6 +12,7 @@ use serenity::{
model::channel::Message, model::channel::Message,
utils::MessageBuilder, utils::MessageBuilder,
}; };
use std::str::FromStr;
use youmubot_prelude::*; use youmubot_prelude::*;
use super::embeds::{beatmap_embed, beatmapset_embed}; use super::embeds::{beatmap_embed, beatmapset_embed};
@ -34,13 +36,13 @@ pub fn hook(ctx: &mut Context, msg: &Message) -> () {
let mut last_beatmap = None; let mut last_beatmap = None;
for l in old_links.into_iter().chain(new_links.into_iter()) { for l in old_links.into_iter().chain(new_links.into_iter()) {
if let Err(v) = msg.channel_id.send_message(&ctx, |m| match l.embed { if let Err(v) = msg.channel_id.send_message(&ctx, |m| match l.embed {
EmbedType::Beatmap(b) => { EmbedType::Beatmap(b, info, mods) => {
let t = handle_beatmap(&b, l.link, l.mode, l.mods, m); let t = handle_beatmap(&b, info, l.link, l.mode, mods, m);
let mode = l.mode.unwrap_or(b.mode); let mode = l.mode.unwrap_or(b.mode);
last_beatmap = Some(super::BeatmapWithMode(b, mode)); last_beatmap = Some(super::BeatmapWithMode(b, mode));
t t
} }
EmbedType::Beatmapset(b) => handle_beatmapset(b, l.link, l.mode, l.mods, m), EmbedType::Beatmapset(b) => handle_beatmapset(b, l.link, l.mode, m),
}) { }) {
println!("Error in osu! hook: {:?}", v) println!("Error in osu! hook: {:?}", v)
} }
@ -59,7 +61,7 @@ pub fn hook(ctx: &mut Context, msg: &Message) -> () {
} }
enum EmbedType { enum EmbedType {
Beatmap(Beatmap), Beatmap(Beatmap, Option<BeatmapInfo>, Mods),
Beatmapset(Vec<Beatmap>), Beatmapset(Vec<Beatmap>),
} }
@ -67,12 +69,12 @@ struct ToPrint<'a> {
embed: EmbedType, embed: EmbedType,
link: &'a str, link: &'a str,
mode: Option<Mode>, mode: Option<Mode>,
mods: Option<&'a str>,
} }
fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPrint<'a>>, Error> { fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPrint<'a>>, Error> {
let osu = ctx.data.get_cloned::<OsuClient>(); let osu = ctx.data.get_cloned::<OsuClient>();
let mut to_prints: Vec<ToPrint<'a>> = Vec::new(); let mut to_prints: Vec<ToPrint<'a>> = Vec::new();
let cache = ctx.data.get_cloned::<BeatmapCache>();
for capture in OLD_LINK_REGEX.captures_iter(content) { for capture in OLD_LINK_REGEX.captures_iter(content) {
let req_type = capture.name("link_type").unwrap().as_str(); let req_type = capture.name("link_type").unwrap().as_str();
let req = match req_type { let req = match req_type {
@ -100,11 +102,22 @@ fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
match req_type { match req_type {
"b" => { "b" => {
for b in beatmaps.into_iter() { for b in beatmaps.into_iter() {
// collect beatmap info
let mods = capture
.name("mods")
.map(|v| Mods::from_str(v.as_str()).ok())
.flatten()
.unwrap_or(Mods::NOMOD);
let info = mode.unwrap_or(b.mode).to_oppai_mode().and_then(|mode| {
cache
.get_beatmap(b.beatmap_id)
.and_then(|b| b.get_info_with(Some(mode), mods))
.ok()
});
to_prints.push(ToPrint { to_prints.push(ToPrint {
embed: EmbedType::Beatmap(b), embed: EmbedType::Beatmap(b, info, mods),
link: capture.get(0).unwrap().as_str(), link: capture.get(0).unwrap().as_str(),
mode, mode,
mods: capture.name("mods").map(|v| v.as_str()),
}) })
} }
} }
@ -112,7 +125,6 @@ fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
embed: EmbedType::Beatmapset(beatmaps), embed: EmbedType::Beatmapset(beatmaps),
link: capture.get(0).unwrap().as_str(), link: capture.get(0).unwrap().as_str(),
mode, mode,
mods: capture.name("mods").map(|v| v.as_str()),
}), }),
_ => (), _ => (),
} }
@ -123,6 +135,7 @@ fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPrint<'a>>, Error> { fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPrint<'a>>, Error> {
let osu = ctx.data.get_cloned::<OsuClient>(); let osu = ctx.data.get_cloned::<OsuClient>();
let mut to_prints: Vec<ToPrint<'a>> = Vec::new(); let mut to_prints: Vec<ToPrint<'a>> = Vec::new();
let cache = ctx.data.get_cloned::<BeatmapCache>();
for capture in NEW_LINK_REGEX.captures_iter(content) { for capture in NEW_LINK_REGEX.captures_iter(content) {
let mode = capture.name("mode").and_then(|v| { let mode = capture.name("mode").and_then(|v| {
Some(match v.as_str() { Some(match v.as_str() {
@ -133,7 +146,6 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
_ => return None, _ => return None,
}) })
}); });
let mods = capture.name("mods").map(|v| v.as_str());
let link = capture.get(0).unwrap().as_str(); let link = capture.get(0).unwrap().as_str();
let req = match capture.name("beatmap_id") { let req = match capture.name("beatmap_id") {
Some(ref v) => BeatmapRequestKind::Beatmap(v.as_str().parse()?), Some(ref v) => BeatmapRequestKind::Beatmap(v.as_str().parse()?),
@ -148,10 +160,24 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
match capture.name("beatmap_id") { match capture.name("beatmap_id") {
Some(_) => { Some(_) => {
for beatmap in beatmaps.into_iter() { for beatmap in beatmaps.into_iter() {
// collect beatmap info
let mods = capture
.name("mods")
.map(|v| Mods::from_str(v.as_str()).ok())
.flatten()
.unwrap_or(Mods::NOMOD);
let info = mode
.unwrap_or(beatmap.mode)
.to_oppai_mode()
.and_then(|mode| {
cache
.get_beatmap(beatmap.beatmap_id)
.and_then(|b| b.get_info_with(Some(mode), mods))
.ok()
});
to_prints.push(ToPrint { to_prints.push(ToPrint {
embed: EmbedType::Beatmap(beatmap), embed: EmbedType::Beatmap(beatmap, info, mods),
link, link,
mods,
mode, mode,
}) })
} }
@ -159,7 +185,6 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
None => to_prints.push(ToPrint { None => to_prints.push(ToPrint {
embed: EmbedType::Beatmapset(beatmaps), embed: EmbedType::Beatmapset(beatmaps),
link, link,
mods,
mode, mode,
}), }),
} }
@ -169,9 +194,10 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
fn handle_beatmap<'a, 'b>( fn handle_beatmap<'a, 'b>(
beatmap: &Beatmap, beatmap: &Beatmap,
info: Option<BeatmapInfo>,
link: &'_ str, link: &'_ str,
mode: Option<Mode>, mode: Option<Mode>,
mods: Option<&'_ str>, mods: Mods,
m: &'a mut CreateMessage<'b>, m: &'a mut CreateMessage<'b>,
) -> &'a mut CreateMessage<'b> { ) -> &'a mut CreateMessage<'b> {
m.content( m.content(
@ -180,14 +206,13 @@ fn handle_beatmap<'a, 'b>(
.push_mono_safe(link) .push_mono_safe(link)
.build(), .build(),
) )
.embed(|b| beatmap_embed(beatmap, mode.unwrap_or(beatmap.mode), b)) .embed(|b| beatmap_embed(beatmap, mode.unwrap_or(beatmap.mode), mods, info, b))
} }
fn handle_beatmapset<'a, 'b>( fn handle_beatmapset<'a, 'b>(
beatmaps: Vec<Beatmap>, beatmaps: Vec<Beatmap>,
link: &'_ str, link: &'_ str,
mode: Option<Mode>, mode: Option<Mode>,
mods: Option<&'_ str>,
m: &'a mut CreateMessage<'b>, m: &'a mut CreateMessage<'b>,
) -> &'a mut CreateMessage<'b> { ) -> &'a mut CreateMessage<'b> {
let mut beatmaps = beatmaps; let mut beatmaps = beatmaps;

View file

@ -1,5 +1,6 @@
use crate::{ use crate::{
models::{Beatmap, Mode, Score, User}, discord::oppai_cache::BeatmapCache,
models::{Beatmap, Mode, Mods, Score, User},
request::{BeatmapRequestKind, UserID}, request::{BeatmapRequestKind, UserID},
Client as OsuHttpClient, Client as OsuHttpClient,
}; };
@ -20,7 +21,7 @@ mod cache;
mod db; mod db;
pub(crate) mod embeds; pub(crate) mod embeds;
mod hook; mod hook;
mod oppai_cache; pub(crate) mod oppai_cache;
mod server_rank; mod server_rank;
use db::OsuUser; use db::OsuUser;
@ -334,18 +335,26 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
#[command] #[command]
#[description = "Show information from the last queried beatmap."] #[description = "Show information from the last queried beatmap."]
#[num_args(0)] #[usage = "[mods = no mod]"]
pub fn last(ctx: &mut Context, msg: &Message, _: Args) -> CommandResult { #[max_args(1)]
pub fn last(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
let b = cache::get_beatmap(&*ctx.data.read(), msg.channel_id)?; let b = cache::get_beatmap(&*ctx.data.read(), msg.channel_id)?;
match b { match b {
Some(BeatmapWithMode(b, m)) => { Some(BeatmapWithMode(b, m)) => {
let mods = args.find::<Mods>().unwrap_or(Mods::NOMOD);
let info = ctx
.data
.get_cloned::<BeatmapCache>()
.get_beatmap(b.beatmap_id)?
.get_info_with(m.to_oppai_mode(), mods)
.ok();
msg.channel_id.send_message(&ctx, |f| { msg.channel_id.send_message(&ctx, |f| {
f.content(format!( f.content(format!(
"{}: here is the beatmap you requested!", "{}: here is the beatmap you requested!",
msg.author msg.author
)) ))
.embed(|c| beatmap_embed(&b, m, c)) .embed(|c| beatmap_embed(&b, m, mods, info, c))
})?; })?;
} }
None => { None => {

View file

@ -12,8 +12,8 @@ pub struct BeatmapContent {
/// the output of "one" oppai run. /// the output of "one" oppai run.
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub struct BeatmapInfo { pub struct BeatmapInfo {
stars: f32, pub stars: f32,
pp: [f32; 4], // 95, 98, 99, 100 pub pp: [f32; 4], // 95, 98, 99, 100
} }
impl BeatmapContent { impl BeatmapContent {
@ -22,21 +22,27 @@ impl BeatmapContent {
&self, &self,
combo: oppai_rs::Combo, combo: oppai_rs::Combo,
accuracy: f32, accuracy: f32,
mode: Option<oppai_rs::Mode>,
mods: impl Into<oppai_rs::Mods>, mods: impl Into<oppai_rs::Mods>,
) -> Result<f32, CommandError> { ) -> Result<f32, CommandError> {
Ok(oppai_rs::Oppai::new_from_content(&self.content[..])? let mut oppai = oppai_rs::Oppai::new_from_content(&self.content[..])?;
.combo(combo)? oppai.combo(combo)?.accuracy(accuracy)?.mods(mods.into());
.accuracy(accuracy)? if let Some(mode) = mode {
.mods(mods.into()) oppai.mode(mode)?;
.pp()) }
Ok(oppai.pp())
} }
/// Get info given mods. /// Get info given mods.
pub fn get_info_with( pub fn get_info_with(
&self, &self,
mode: Option<oppai_rs::Mode>,
mods: impl Into<oppai_rs::Mods>, mods: impl Into<oppai_rs::Mods>,
) -> Result<BeatmapInfo, CommandError> { ) -> Result<BeatmapInfo, CommandError> {
let mut oppai = oppai_rs::Oppai::new_from_content(&self.content[..])?; let mut oppai = oppai_rs::Oppai::new_from_content(&self.content[..])?;
if let Some(mode) = mode {
oppai.mode(mode)?;
}
oppai.mods(mods.into()).combo(oppai_rs::Combo::PERFECT)?; oppai.mods(mods.into()).combo(oppai_rs::Combo::PERFECT)?;
let pp = [ let pp = [
oppai.accuracy(95.0)?.pp(), oppai.accuracy(95.0)?.pp(),
@ -50,6 +56,7 @@ impl BeatmapContent {
} }
/// A central cache for the beatmaps. /// A central cache for the beatmaps.
#[derive(Clone, Debug)]
pub struct BeatmapCache { pub struct BeatmapCache {
client: reqwest::blocking::Client, client: reqwest::blocking::Client,
cache: Arc<dashmap::DashMap<u64, BeatmapContent>>, cache: Arc<dashmap::DashMap<u64, BeatmapContent>>,
@ -67,7 +74,7 @@ impl BeatmapCache {
fn download_beatmap(&self, id: u64) -> Result<BeatmapContent, CommandError> { fn download_beatmap(&self, id: u64) -> Result<BeatmapContent, CommandError> {
let content = self let content = self
.client .client
.get(&format!("https://osu.ppy.sh/u/{}", id)) .get(&format!("https://osu.ppy.sh/osu/{}", id))
.send()? .send()?
.bytes()?; .bytes()?;
Ok(BeatmapContent { Ok(BeatmapContent {

View file

@ -118,6 +118,17 @@ impl fmt::Display for Mode {
} }
} }
impl Mode {
/// Convert to oppai mode.
pub fn to_oppai_mode(self) -> Option<oppai_rs::Mode> {
Some(match self {
Mode::Std => oppai_rs::Mode::Std,
Mode::Taiko => oppai_rs::Mode::Taiko,
_ => return None,
})
}
}
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug, Serialize, Deserialize)]
pub struct Beatmap { pub struct Beatmap {
// Beatmapset info // Beatmapset info