mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 00:38:54 +00:00
Merge pull request 'oppai integration and lots of osu! improvements' (#21) from oppai-integration into master
This commit is contained in:
commit
200416765a
16 changed files with 835 additions and 164 deletions
|
@ -10,12 +10,12 @@ trigger:
|
|||
|
||||
steps:
|
||||
- name: format_check
|
||||
image: rust:1.41
|
||||
image: rust:1.44
|
||||
commands:
|
||||
- rustup component add rustfmt
|
||||
- cargo fmt -- --check
|
||||
- name: cargo_check
|
||||
image: rust:1.41
|
||||
image: rust:1.44
|
||||
commands:
|
||||
- cargo check
|
||||
|
||||
|
@ -31,7 +31,7 @@ trigger:
|
|||
|
||||
steps:
|
||||
- name: build_release
|
||||
image: rust:1.41
|
||||
image: rust:1.44
|
||||
commands:
|
||||
- cargo build --release
|
||||
- name: deploy
|
||||
|
|
53
Cargo.lock
generated
53
Cargo.lock
generated
|
@ -22,6 +22,14 @@ name = "adler32"
|
|||
version = "1.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.3.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"const-random 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "aho-corasick"
|
||||
version = "0.7.10"
|
||||
|
@ -162,6 +170,24 @@ dependencies = [
|
|||
"syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"const-random-macro 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "const-random-macro"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"getrandom 0.1.14 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"proc-macro-hack 0.5.15 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.7.0"
|
||||
|
@ -244,6 +270,16 @@ dependencies = [
|
|||
"sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dashmap"
|
||||
version = "3.11.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"ahash 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cfg-if 0.1.10 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"num_cpus 1.13.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.8.1"
|
||||
|
@ -796,6 +832,16 @@ dependencies = [
|
|||
"vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "oppai-rs"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
dependencies = [
|
||||
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"cc 1.0.54 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"libc 0.2.70 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "parking_lot"
|
||||
version = "0.9.0"
|
||||
|
@ -1710,7 +1756,9 @@ version = "0.1.0"
|
|||
dependencies = [
|
||||
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"chrono 0.4.11 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"dashmap 3.11.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"lazy_static 1.4.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"oppai-rs 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"rayon 1.3.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"regex 1.3.7 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"reqwest 0.10.4 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
|
@ -1737,6 +1785,7 @@ dependencies = [
|
|||
"checksum Inflector 0.11.4 (registry+https://github.com/rust-lang/crates.io-index)" = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3"
|
||||
"checksum addr2line 0.12.1 (registry+https://github.com/rust-lang/crates.io-index)" = "a49806b9dadc843c61e7c97e72490ad7f7220ae249012fbda9ad0609457c0543"
|
||||
"checksum adler32 1.0.4 (registry+https://github.com/rust-lang/crates.io-index)" = "5d2e7343e7fc9de883d1b0341e0b13970f764c14101234857d2ddafa1cb1cac2"
|
||||
"checksum ahash 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "e8fd72866655d1904d6b0997d0b07ba561047d070fbe29de039031c641b61217"
|
||||
"checksum aho-corasick 0.7.10 (registry+https://github.com/rust-lang/crates.io-index)" = "8716408b8bc624ed7f65d223ddb9ac2d044c0547b6fa4b0d554f3a9540496ada"
|
||||
"checksum autocfg 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f8aac770f1885fd7e387acedd76065302551364496e46b3dd00860b2f8359b9d"
|
||||
"checksum backtrace 0.3.48 (registry+https://github.com/rust-lang/crates.io-index)" = "0df2f85c8a2abbe3b7d7e748052fdd9b76a0458fdeb16ad4223f5eca78c7c130"
|
||||
|
@ -1756,6 +1805,8 @@ dependencies = [
|
|||
"checksum cloudabi 0.0.3 (registry+https://github.com/rust-lang/crates.io-index)" = "ddfc5b9aa5d4507acaf872de71051dfd0e309860e88966e1051e462a077aac4f"
|
||||
"checksum codeforces 0.1.0 (git+https://github.com/natsukagami/rust-codeforces-api)" = "<none>"
|
||||
"checksum command_attr 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "c27d6155f93d880b6379d93ddc9b2417b3b69b715360c5f25525e4576338a381"
|
||||
"checksum const-random 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "2f1af9ac737b2dd2d577701e59fd09ba34822f6f2ebdb30a7647405d9e55e16a"
|
||||
"checksum const-random-macro 0.1.8 (registry+https://github.com/rust-lang/crates.io-index)" = "25e4c606eb459dd29f7c57b2e0879f2b6f14ee130918c2b78ccb58a9624e6c7a"
|
||||
"checksum core-foundation 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "57d24c7a13c43e870e37c1556b74555437870a04514f7685f5b354e090567171"
|
||||
"checksum core-foundation-sys 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "b3a71ab494c0b5b860bdc8407ae08978052417070c2ced38573a9157ad75b8ac"
|
||||
"checksum crc32fast 1.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "ba125de2af0df55319f41944744ad91c71113bf74a4646efff39afe1f6842db1"
|
||||
|
@ -1765,6 +1816,7 @@ dependencies = [
|
|||
"checksum crossbeam-queue 0.2.1 (registry+https://github.com/rust-lang/crates.io-index)" = "c695eeca1e7173472a32221542ae469b3e9aac3a4fc81f7696bcad82029493db"
|
||||
"checksum crossbeam-utils 0.7.2 (registry+https://github.com/rust-lang/crates.io-index)" = "c3c7c73a2d1e9fc0886a08b93e98eb643461230d5f1925e4036204d5f2e261a8"
|
||||
"checksum ct-logs 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4d3686f5fa27dbc1d76c751300376e167c5a43387f44bb451fd1c24776e49113"
|
||||
"checksum dashmap 3.11.4 (registry+https://github.com/rust-lang/crates.io-index)" = "8cfcd41ae02d60edded204341d2798ba519c336c51a37330aa4b98a1128def32"
|
||||
"checksum digest 0.8.1 (registry+https://github.com/rust-lang/crates.io-index)" = "f3d0c8c8752312f9713efd397ff63acb9f85585afbf179282e720e7704954dd5"
|
||||
"checksum dotenv 0.15.0 (registry+https://github.com/rust-lang/crates.io-index)" = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
"checksum dtoa 0.4.5 (registry+https://github.com/rust-lang/crates.io-index)" = "4358a9e11b9a09cf52383b451b49a169e8d797b68aa02301ff586d70d9661ea3"
|
||||
|
@ -1830,6 +1882,7 @@ dependencies = [
|
|||
"checksum openssl 0.10.29 (registry+https://github.com/rust-lang/crates.io-index)" = "cee6d85f4cb4c4f59a6a85d5b68a233d280c82e29e822913b9c8b129fbf20bdd"
|
||||
"checksum openssl-probe 0.1.2 (registry+https://github.com/rust-lang/crates.io-index)" = "77af24da69f9d9341038eba93a073b1fdaaa1b788221b00a69bce9e762cb32de"
|
||||
"checksum openssl-sys 0.9.56 (registry+https://github.com/rust-lang/crates.io-index)" = "f02309a7f127000ed50594f0b50ecc69e7c654e16d41b4e8156d1b3df8e0b52e"
|
||||
"checksum oppai-rs 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)" = "4f143357550da5c04800333509df440fcbe5254120a5af05097a083caf23105b"
|
||||
"checksum parking_lot 0.9.0 (registry+https://github.com/rust-lang/crates.io-index)" = "f842b1982eb6c2fe34036a4fbfb06dd185a3f5c8edfaacdf7d1ea10b07de6252"
|
||||
"checksum parking_lot_core 0.6.2 (registry+https://github.com/rust-lang/crates.io-index)" = "b876b1b9e7ac6e1a74a6da34d25c42e17e8862aa409cbbbdcfc8d86c6f3bc62b"
|
||||
"checksum percent-encoding 2.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
|
||||
|
|
|
@ -15,6 +15,8 @@ bitflags = "1"
|
|||
rayon = "1.1"
|
||||
lazy_static = "1"
|
||||
regex = "1"
|
||||
oppai-rs = "0.2.0"
|
||||
dashmap = "3.11.4"
|
||||
|
||||
youmubot-db = { path = "../youmubot-db" }
|
||||
youmubot-prelude = { path = "../youmubot-prelude" }
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
use super::db::{OsuSavedUsers, OsuUser};
|
||||
use super::{embeds::score_embed, BeatmapWithMode, OsuClient};
|
||||
use crate::{
|
||||
discord::beatmap_cache::BeatmapMetaCache,
|
||||
discord::oppai_cache::BeatmapCache,
|
||||
models::{Mode, Score},
|
||||
request::{BeatmapRequestKind, UserID},
|
||||
request::UserID,
|
||||
Client as Osu,
|
||||
};
|
||||
use announcer::MemberToChannels;
|
||||
|
@ -22,6 +24,8 @@ pub const ANNOUNCER_KEY: &'static str = "osu";
|
|||
/// Announce osu! top scores.
|
||||
pub fn updates(c: Arc<CacheAndHttp>, d: AppData, channels: MemberToChannels) -> CommandResult {
|
||||
let osu = d.get_cloned::<OsuClient>();
|
||||
let cache = d.get_cloned::<BeatmapMetaCache>();
|
||||
let oppai = d.get_cloned::<BeatmapCache>();
|
||||
// For each user...
|
||||
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
||||
for (user_id, osu_user) in data.iter_mut() {
|
||||
|
@ -31,7 +35,18 @@ pub fn updates(c: Arc<CacheAndHttp>, d: AppData, channels: MemberToChannels) ->
|
|||
}
|
||||
osu_user.pp = match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania])
|
||||
.par_iter()
|
||||
.map(|m| handle_user_mode(c.clone(), &osu, &osu_user, *user_id, &channels[..], *m))
|
||||
.map(|m| {
|
||||
handle_user_mode(
|
||||
c.clone(),
|
||||
&osu,
|
||||
&cache,
|
||||
&oppai,
|
||||
&osu_user,
|
||||
*user_id,
|
||||
&channels[..],
|
||||
*m,
|
||||
)
|
||||
})
|
||||
.collect::<Result<_, _>>()
|
||||
{
|
||||
Ok(v) => v,
|
||||
|
@ -51,6 +66,8 @@ pub fn updates(c: Arc<CacheAndHttp>, d: AppData, channels: MemberToChannels) ->
|
|||
fn handle_user_mode(
|
||||
c: Arc<CacheAndHttp>,
|
||||
osu: &Osu,
|
||||
cache: &BeatmapMetaCache,
|
||||
oppai: &BeatmapCache,
|
||||
osu_user: &OsuUser,
|
||||
user_id: UserId,
|
||||
channels: &[ChannelId],
|
||||
|
@ -62,23 +79,17 @@ fn handle_user_mode(
|
|||
.ok_or(Error::from("user not found"))?;
|
||||
scores
|
||||
.into_par_iter()
|
||||
.filter_map(|(rank, score)| {
|
||||
let beatmap = osu
|
||||
.beatmaps(BeatmapRequestKind::Beatmap(score.beatmap_id), |f| f)
|
||||
.map(|v| BeatmapWithMode(v.into_iter().next().unwrap(), mode));
|
||||
match beatmap {
|
||||
Ok(v) => Some((rank, score, v)),
|
||||
Err(e) => {
|
||||
dbg!(e);
|
||||
None
|
||||
}
|
||||
}
|
||||
.map(|(rank, score)| -> Result<_, Error> {
|
||||
let beatmap = cache.get_beatmap_default(score.beatmap_id)?;
|
||||
let content = oppai.get_beatmap(beatmap.beatmap_id)?;
|
||||
Ok((rank, score, BeatmapWithMode(beatmap, mode), content))
|
||||
})
|
||||
.for_each(|(rank, score, beatmap)| {
|
||||
.filter_map(|v| v.ok())
|
||||
.for_each(|(rank, score, beatmap, content)| {
|
||||
for channel in (&channels).iter() {
|
||||
if let Err(e) = channel.send_message(c.http(), |c| {
|
||||
c.content(format!("New top record from {}!", user_id.mention()))
|
||||
.embed(|e| score_embed(&score, &beatmap, &user, Some(rank), e))
|
||||
.embed(|e| score_embed(&score, &beatmap, &content, &user, Some(rank), e))
|
||||
}) {
|
||||
dbg!(e);
|
||||
}
|
||||
|
|
70
youmubot-osu/src/discord/beatmap_cache.rs
Normal file
70
youmubot-osu/src/discord/beatmap_cache.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
use crate::{
|
||||
models::{ApprovalStatus, Beatmap, Mode},
|
||||
Client,
|
||||
};
|
||||
use dashmap::DashMap;
|
||||
use serenity::framework::standard::CommandError;
|
||||
use std::sync::Arc;
|
||||
use youmubot_prelude::TypeMapKey;
|
||||
|
||||
/// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling.
|
||||
/// Does not cache non-Ranked beatmaps.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BeatmapMetaCache {
|
||||
client: Client,
|
||||
cache: Arc<DashMap<(u64, Mode), Beatmap>>,
|
||||
}
|
||||
|
||||
impl TypeMapKey for BeatmapMetaCache {
|
||||
type Value = BeatmapMetaCache;
|
||||
}
|
||||
|
||||
impl BeatmapMetaCache {
|
||||
/// Create a new beatmap cache.
|
||||
pub fn new(client: Client) -> Self {
|
||||
BeatmapMetaCache {
|
||||
client,
|
||||
cache: Arc::new(DashMap::new()),
|
||||
}
|
||||
}
|
||||
fn insert_if_possible(&self, id: u64, mode: Option<Mode>) -> Result<Beatmap, CommandError> {
|
||||
let beatmap = self
|
||||
.client
|
||||
.beatmaps(crate::BeatmapRequestKind::Beatmap(id), |f| {
|
||||
if let Some(mode) = mode {
|
||||
f.mode(mode, true);
|
||||
}
|
||||
f
|
||||
})
|
||||
.and_then(|v| {
|
||||
v.into_iter()
|
||||
.next()
|
||||
.ok_or(CommandError::from("beatmap not found"))
|
||||
})?;
|
||||
if let ApprovalStatus::Ranked(_) = beatmap.approval {
|
||||
self.cache.insert((id, beatmap.mode), beatmap.clone());
|
||||
};
|
||||
Ok(beatmap)
|
||||
}
|
||||
/// Get the given beatmap
|
||||
pub fn get_beatmap(&self, id: u64, mode: Mode) -> Result<Beatmap, CommandError> {
|
||||
self.cache
|
||||
.get(&(id, mode))
|
||||
.map(|b| Ok(b.clone()))
|
||||
.unwrap_or_else(|| self.insert_if_possible(id, Some(mode)))
|
||||
}
|
||||
|
||||
/// Get a beatmap without a mode...
|
||||
pub fn get_beatmap_default(&self, id: u64) -> Result<Beatmap, CommandError> {
|
||||
(&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania])
|
||||
.iter()
|
||||
.filter_map(|&mode| {
|
||||
self.cache
|
||||
.get(&(id, mode))
|
||||
.filter(|b| b.mode == mode)
|
||||
.map(|b| Ok(b.clone()))
|
||||
})
|
||||
.next()
|
||||
.unwrap_or_else(|| self.insert_if_possible(id, None))
|
||||
}
|
||||
}
|
|
@ -1,5 +1,8 @@
|
|||
use super::BeatmapWithMode;
|
||||
use crate::models::{Beatmap, Mode, Rank, Score, User};
|
||||
use crate::{
|
||||
discord::oppai_cache::{BeatmapContent, BeatmapInfo},
|
||||
models::{Beatmap, Mode, Mods, Rank, Score, User},
|
||||
};
|
||||
use chrono::Utc;
|
||||
use serenity::{builder::CreateEmbed, utils::MessageBuilder};
|
||||
use youmubot_prelude::*;
|
||||
|
@ -12,7 +15,19 @@ 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 diff = b.difficulty.apply_mods(mods);
|
||||
c.title(
|
||||
MessageBuilder::new()
|
||||
.push_bold_safe(&b.artist)
|
||||
|
@ -21,6 +36,7 @@ pub fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a
|
|||
.push(" [")
|
||||
.push_bold_safe(&b.difficulty_name)
|
||||
.push("]")
|
||||
.push(&mod_str)
|
||||
.build(),
|
||||
)
|
||||
.author(|a| {
|
||||
|
@ -34,27 +50,37 @@ pub fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a
|
|||
.color(0xffb6c1)
|
||||
.field(
|
||||
"Star Difficulty",
|
||||
format!("{:.2}⭐", b.difficulty.stars),
|
||||
false,
|
||||
format!(
|
||||
"{:.2}⭐",
|
||||
info.map(|v| v.stars as f64).unwrap_or(b.difficulty.stars)
|
||||
),
|
||||
true,
|
||||
)
|
||||
.fields(Some(("Mods", mods, true)).filter(|_| mods != Mods::NOMOD))
|
||||
.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,
|
||||
)
|
||||
}))
|
||||
.field(
|
||||
"Length",
|
||||
MessageBuilder::new()
|
||||
.push_bold_safe(Duration(b.total_length))
|
||||
.push_bold_safe(Duration(diff.total_length))
|
||||
.push(" (")
|
||||
.push_bold_safe(Duration(b.drain_length))
|
||||
.push_bold_safe(Duration(diff.drain_length))
|
||||
.push(" drain)")
|
||||
.build(),
|
||||
false,
|
||||
)
|
||||
.field("Circle Size", format!("{:.1}", b.difficulty.cs), true)
|
||||
.field("Approach Rate", format!("{:.1}", b.difficulty.ar), true)
|
||||
.field(
|
||||
"Overall Difficulty",
|
||||
format!("{:.1}", b.difficulty.od),
|
||||
true,
|
||||
)
|
||||
.field("HP Drain", format!("{:.1}", b.difficulty.hp), true)
|
||||
.field("Circle Size", format!("{:.1}", diff.cs), true)
|
||||
.field("Approach Rate", format!("{:.1}", diff.ar), true)
|
||||
.field("Overall Difficulty", format!("{:.1}", diff.od), true)
|
||||
.field("HP Drain", format!("{:.1}", diff.hp), true)
|
||||
.field("BPM", b.bpm.round(), true)
|
||||
.fields(b.difficulty.max_combo.map(|v| ("Max combo", v, true)))
|
||||
.field("Mode", format_mode(m, b.mode), true)
|
||||
|
@ -83,13 +109,23 @@ pub fn beatmap_embed<'a>(b: &'_ Beatmap, m: Mode, c: &'a mut CreateEmbed) -> &'a
|
|||
)
|
||||
})
|
||||
.push_line(format!(" [[Beatmapset]]({})", b.beatmapset_link()))
|
||||
.push_line(&b.approval)
|
||||
.push_line(format!(
|
||||
"Short link: `{}`",
|
||||
b.short_link(Some(m), Some(mods))
|
||||
))
|
||||
.push_bold_line(&b.approval)
|
||||
.push("Language: ")
|
||||
.push_bold(&b.language)
|
||||
.push(" | Genre: ")
|
||||
.push_bold(&b.genre)
|
||||
.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;
|
||||
|
@ -145,7 +181,7 @@ pub fn beatmapset_embed<'a>(
|
|||
.field(
|
||||
"Length",
|
||||
MessageBuilder::new()
|
||||
.push_bold_safe(Duration(b.total_length))
|
||||
.push_bold_safe(Duration(b.difficulty.total_length))
|
||||
.build(),
|
||||
true,
|
||||
)
|
||||
|
@ -177,7 +213,11 @@ pub fn beatmapset_embed<'a>(
|
|||
(
|
||||
format!("[{}]", b.difficulty_name),
|
||||
MessageBuilder::new()
|
||||
.push(format!("[[Link]]({})", b.link()))
|
||||
.push(format!(
|
||||
"[[Link]]({}) (`{}`)",
|
||||
b.link(),
|
||||
b.short_link(m, None)
|
||||
))
|
||||
.push(", ")
|
||||
.push_bold(format!("{:.2}⭐", b.difficulty.stars))
|
||||
.push(", ")
|
||||
|
@ -191,7 +231,7 @@ pub fn beatmapset_embed<'a>(
|
|||
.push(", HP")
|
||||
.push_bold(format!("{:.1}", b.difficulty.hp))
|
||||
.push(", ⌛ ")
|
||||
.push_bold(format!("{}", Duration(b.drain_length)))
|
||||
.push_bold(format!("{}", Duration(b.difficulty.drain_length)))
|
||||
.build(),
|
||||
false,
|
||||
)
|
||||
|
@ -201,6 +241,7 @@ pub fn beatmapset_embed<'a>(
|
|||
pub(crate) fn score_embed<'a>(
|
||||
s: &Score,
|
||||
bm: &BeatmapWithMode,
|
||||
content: &BeatmapContent,
|
||||
u: &User,
|
||||
top_record: Option<u8>,
|
||||
m: &'a mut CreateEmbed,
|
||||
|
@ -208,6 +249,11 @@ pub(crate) fn score_embed<'a>(
|
|||
let mode = bm.mode();
|
||||
let b = &bm.0;
|
||||
let accuracy = s.accuracy(mode);
|
||||
let stars = mode
|
||||
.to_oppai_mode()
|
||||
.and_then(|mode| content.get_info_with(Some(mode), s.mods).ok())
|
||||
.map(|info| info.stars as f64)
|
||||
.unwrap_or(b.difficulty.stars);
|
||||
let score_line = match &s.rank {
|
||||
Rank::SS | Rank::SSH => format!("SS"),
|
||||
_ if s.perfect => format!("{:.2}% FC", accuracy),
|
||||
|
@ -217,12 +263,27 @@ pub(crate) fn score_embed<'a>(
|
|||
accuracy, s.max_combo, s.count_miss, v
|
||||
),
|
||||
};
|
||||
let score_line =
|
||||
s.pp.map(|pp| format!("{} | {:.2}pp", &score_line, pp))
|
||||
.unwrap_or(score_line);
|
||||
let pp = s.pp.map(|pp| format!("{:.2}pp", pp)).or_else(|| {
|
||||
mode.to_oppai_mode()
|
||||
.and_then(|op| {
|
||||
content
|
||||
.get_pp_from(
|
||||
oppai_rs::Combo::non_fc(s.max_combo as u32, s.count_miss as u32),
|
||||
accuracy as f32,
|
||||
Some(op),
|
||||
s.mods,
|
||||
)
|
||||
.ok()
|
||||
})
|
||||
.map(|pp| format!("{:.2}pp [?]", pp))
|
||||
});
|
||||
let score_line = pp
|
||||
.map(|pp| format!("{} | {}", &score_line, pp))
|
||||
.unwrap_or(score_line);
|
||||
let top_record = top_record
|
||||
.map(|v| format!("| #{} top record!", v))
|
||||
.unwrap_or("".to_owned());
|
||||
let diff = b.difficulty.apply_mods(s.mods);
|
||||
m.author(|f| f.name(&u.username).url(u.link()).icon_url(u.avatar_url()))
|
||||
.color(0xffb6c1)
|
||||
.title(format!(
|
||||
|
@ -232,7 +293,7 @@ pub(crate) fn score_embed<'a>(
|
|||
b.title,
|
||||
b.difficulty_name,
|
||||
s.mods,
|
||||
b.difficulty.stars,
|
||||
stars,
|
||||
b.creator,
|
||||
score_line,
|
||||
top_record
|
||||
|
@ -245,15 +306,26 @@ pub(crate) fn score_embed<'a>(
|
|||
false,
|
||||
)
|
||||
.field("Rank", &score_line, false)
|
||||
.field(
|
||||
"300s / 100s / 50s / misses",
|
||||
format!(
|
||||
"**{}** ({}) / **{}** ({}) / **{}** / **{}**",
|
||||
s.count_300, s.count_geki, s.count_100, s.count_katu, s.count_50, s.count_miss
|
||||
),
|
||||
true,
|
||||
)
|
||||
.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(format!(
|
||||
"[[Link]]({}) (`{}`)",
|
||||
b.link(),
|
||||
b.short_link(Some(mode), Some(s.mods))
|
||||
))
|
||||
.push(", ")
|
||||
.push_bold(format!("{:.2}⭐", b.difficulty.stars))
|
||||
.push_bold(format!("{:.2}⭐", stars))
|
||||
.push(", ")
|
||||
.push_bold_line(
|
||||
b.mode.to_string()
|
||||
|
@ -264,24 +336,29 @@ pub(crate) fn score_embed<'a>(
|
|||
},
|
||||
)
|
||||
.push("CS")
|
||||
.push_bold(format!("{:.1}", b.difficulty.cs))
|
||||
.push_bold(format!("{:.1}", diff.cs))
|
||||
.push(", AR")
|
||||
.push_bold(format!("{:.1}", b.difficulty.ar))
|
||||
.push_bold(format!("{:.1}", diff.ar))
|
||||
.push(", OD")
|
||||
.push_bold(format!("{:.1}", b.difficulty.od))
|
||||
.push_bold(format!("{:.1}", diff.od))
|
||||
.push(", HP")
|
||||
.push_bold(format!("{:.1}", b.difficulty.hp))
|
||||
.push_bold(format!("{:.1}", diff.hp))
|
||||
.push(", ⌛ ")
|
||||
.push_bold(format!("{}", Duration(b.drain_length)))
|
||||
.push_bold(format!("{}", Duration(diff.drain_length)))
|
||||
.build(),
|
||||
false,
|
||||
)
|
||||
.field("Played on", s.date.format("%F %T"), false)
|
||||
.timestamp(&s.date)
|
||||
.field("Played on", s.date.format("%F %T"), false);
|
||||
if mode.to_oppai_mode().is_none() && s.mods != Mods::NOMOD {
|
||||
m.footer(|f| f.text("Star difficulty does not reflect game mods."));
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
pub(crate) fn user_embed<'a>(
|
||||
u: User,
|
||||
best: Option<(Score, BeatmapWithMode)>,
|
||||
best: Option<(Score, BeatmapWithMode, Option<BeatmapInfo>)>,
|
||||
m: &'a mut CreateEmbed,
|
||||
) -> &'a mut CreateEmbed {
|
||||
m.title(u.username)
|
||||
|
@ -323,8 +400,8 @@ pub(crate) fn user_embed<'a>(
|
|||
),
|
||||
false,
|
||||
)
|
||||
.fields(best.map(|(v, map)| {
|
||||
let map = map.0;
|
||||
.fields(best.map(|(v, map, info)| {
|
||||
let BeatmapWithMode(map, mode) = map;
|
||||
(
|
||||
"Best Record",
|
||||
MessageBuilder::new()
|
||||
|
@ -348,8 +425,12 @@ pub(crate) fn user_embed<'a>(
|
|||
MessageBuilder::new().push_bold_safe(&map.title).build(),
|
||||
map.link()
|
||||
))
|
||||
.push(format!(" [{}]", map.difficulty_name))
|
||||
.push(format!(" ({:.1}⭐)", map.difficulty.stars))
|
||||
.push_line(format!(" [{}]", map.difficulty_name))
|
||||
.push(format!(
|
||||
"{:.1}⭐ | `{}`",
|
||||
info.map(|i| i.stars as f64).unwrap_or(map.difficulty.stars),
|
||||
map.short_link(Some(mode), Some(v.mods))
|
||||
))
|
||||
.build(),
|
||||
false,
|
||||
)
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
use super::OsuClient;
|
||||
use crate::{
|
||||
models::{Beatmap, Mode},
|
||||
discord::beatmap_cache::BeatmapMetaCache,
|
||||
discord::oppai_cache::{BeatmapCache, BeatmapInfo},
|
||||
models::{Beatmap, Mode, Mods},
|
||||
request::BeatmapRequestKind,
|
||||
};
|
||||
use lazy_static::lazy_static;
|
||||
|
@ -11,6 +13,7 @@ use serenity::{
|
|||
model::channel::Message,
|
||||
utils::MessageBuilder,
|
||||
};
|
||||
use std::str::FromStr;
|
||||
use youmubot_prelude::*;
|
||||
|
||||
use super::embeds::{beatmap_embed, beatmapset_embed};
|
||||
|
@ -22,6 +25,9 @@ lazy_static! {
|
|||
static ref NEW_LINK_REGEX: Regex = Regex::new(
|
||||
r"https?://osu\.ppy\.sh/beatmapsets/(?P<set_id>\d+)/?(?:\#(?P<mode>osu|taiko|fruits|mania)(?:/(?P<beatmap_id>\d+)|/?))?(?:\+(?P<mods>[A-Z]+))?"
|
||||
).unwrap();
|
||||
static ref SHORT_LINK_REGEX: Regex = Regex::new(
|
||||
r"/b/(?P<id>\d+)(?:/(?P<mode>osu|taiko|fruits|mania))?(?:\+(?P<mods>[A-Z]+))?"
|
||||
).unwrap();
|
||||
}
|
||||
|
||||
pub fn hook(ctx: &mut Context, msg: &Message) -> () {
|
||||
|
@ -31,16 +37,21 @@ pub fn hook(ctx: &mut Context, msg: &Message) -> () {
|
|||
let mut v = move || -> CommandResult {
|
||||
let old_links = handle_old_links(ctx, &msg.content)?;
|
||||
let new_links = handle_new_links(ctx, &msg.content)?;
|
||||
let short_links = handle_short_links(ctx, &msg, &msg.content)?;
|
||||
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())
|
||||
.chain(short_links.into_iter())
|
||||
{
|
||||
if let Err(v) = msg.channel_id.send_message(&ctx, |m| match l.embed {
|
||||
EmbedType::Beatmap(b) => {
|
||||
let t = handle_beatmap(&b, l.link, l.mode, l.mods, m);
|
||||
EmbedType::Beatmap(b, info, mods) => {
|
||||
let t = handle_beatmap(&b, info, l.link, l.mode, mods, m);
|
||||
let mode = l.mode.unwrap_or(b.mode);
|
||||
last_beatmap = Some(super::BeatmapWithMode(b, mode));
|
||||
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)
|
||||
}
|
||||
|
@ -59,7 +70,7 @@ pub fn hook(ctx: &mut Context, msg: &Message) -> () {
|
|||
}
|
||||
|
||||
enum EmbedType {
|
||||
Beatmap(Beatmap),
|
||||
Beatmap(Beatmap, Option<BeatmapInfo>, Mods),
|
||||
Beatmapset(Vec<Beatmap>),
|
||||
}
|
||||
|
||||
|
@ -67,12 +78,12 @@ struct ToPrint<'a> {
|
|||
embed: EmbedType,
|
||||
link: &'a str,
|
||||
mode: Option<Mode>,
|
||||
mods: Option<&'a str>,
|
||||
}
|
||||
|
||||
fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPrint<'a>>, Error> {
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
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) {
|
||||
let req_type = capture.name("link_type").unwrap().as_str();
|
||||
let req = match req_type {
|
||||
|
@ -100,11 +111,22 @@ fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
|
|||
match req_type {
|
||||
"b" => {
|
||||
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 {
|
||||
embed: EmbedType::Beatmap(b),
|
||||
embed: EmbedType::Beatmap(b, info, mods),
|
||||
link: capture.get(0).unwrap().as_str(),
|
||||
mode,
|
||||
mods: capture.name("mods").map(|v| v.as_str()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -112,7 +134,6 @@ fn handle_old_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
|
|||
embed: EmbedType::Beatmapset(beatmaps),
|
||||
link: capture.get(0).unwrap().as_str(),
|
||||
mode,
|
||||
mods: capture.name("mods").map(|v| v.as_str()),
|
||||
}),
|
||||
_ => (),
|
||||
}
|
||||
|
@ -123,17 +144,11 @@ 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> {
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
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) {
|
||||
let mode = capture.name("mode").and_then(|v| {
|
||||
Some(match v.as_str() {
|
||||
"osu" => Mode::Std,
|
||||
"taiko" => Mode::Taiko,
|
||||
"fruits" => Mode::Catch,
|
||||
"mania" => Mode::Mania,
|
||||
_ => return None,
|
||||
})
|
||||
});
|
||||
let mods = capture.name("mods").map(|v| v.as_str());
|
||||
let mode = capture
|
||||
.name("mode")
|
||||
.and_then(|v| Mode::parse_from_new_site(v.as_str()));
|
||||
let link = capture.get(0).unwrap().as_str();
|
||||
let req = match capture.name("beatmap_id") {
|
||||
Some(ref v) => BeatmapRequestKind::Beatmap(v.as_str().parse()?),
|
||||
|
@ -148,10 +163,23 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
|
|||
match capture.name("beatmap_id") {
|
||||
Some(_) => {
|
||||
for beatmap in beatmaps.into_iter() {
|
||||
// collect beatmap info
|
||||
let mods = capture
|
||||
.name("mods")
|
||||
.and_then(|v| Mods::from_str(v.as_str()).ok())
|
||||
.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 {
|
||||
embed: EmbedType::Beatmap(beatmap),
|
||||
embed: EmbedType::Beatmap(beatmap, info, mods),
|
||||
link,
|
||||
mods,
|
||||
mode,
|
||||
})
|
||||
}
|
||||
|
@ -159,7 +187,6 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
|
|||
None => to_prints.push(ToPrint {
|
||||
embed: EmbedType::Beatmapset(beatmaps),
|
||||
link,
|
||||
mods,
|
||||
mode,
|
||||
}),
|
||||
}
|
||||
|
@ -167,11 +194,61 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
|
|||
Ok(to_prints)
|
||||
}
|
||||
|
||||
fn handle_short_links<'a>(
|
||||
ctx: &mut Context,
|
||||
msg: &Message,
|
||||
content: &'a str,
|
||||
) -> Result<Vec<ToPrint<'a>>, Error> {
|
||||
if let Some(guild_id) = msg.guild_id {
|
||||
if announcer::announcer_of(ctx, crate::discord::announcer::ANNOUNCER_KEY, guild_id)?
|
||||
!= Some(msg.channel_id)
|
||||
{
|
||||
// Disable if we are not in the server's announcer channel
|
||||
return Ok(vec![]);
|
||||
}
|
||||
}
|
||||
let osu = ctx.data.get_cloned::<BeatmapMetaCache>();
|
||||
let cache = ctx.data.get_cloned::<BeatmapCache>();
|
||||
Ok(SHORT_LINK_REGEX
|
||||
.captures_iter(content)
|
||||
.map(|capture| -> Result<_, Error> {
|
||||
let mode = capture
|
||||
.name("mode")
|
||||
.and_then(|v| Mode::parse_from_new_site(v.as_str()));
|
||||
let id: u64 = capture.name("id").unwrap().as_str().parse()?;
|
||||
let beatmap = match mode {
|
||||
Some(mode) => osu.get_beatmap(id, mode),
|
||||
None => osu.get_beatmap_default(id),
|
||||
}?;
|
||||
let mods = capture
|
||||
.name("mods")
|
||||
.and_then(|v| Mods::from_str(v.as_str()).ok())
|
||||
.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()
|
||||
});
|
||||
Ok(ToPrint {
|
||||
embed: EmbedType::Beatmap(beatmap, info, mods),
|
||||
link: capture.get(0).unwrap().as_str(),
|
||||
mode,
|
||||
})
|
||||
})
|
||||
.filter_map(|v| v.ok())
|
||||
.collect())
|
||||
}
|
||||
|
||||
fn handle_beatmap<'a, 'b>(
|
||||
beatmap: &Beatmap,
|
||||
info: Option<BeatmapInfo>,
|
||||
link: &'_ str,
|
||||
mode: Option<Mode>,
|
||||
mods: Option<&'_ str>,
|
||||
mods: Mods,
|
||||
m: &'a mut CreateMessage<'b>,
|
||||
) -> &'a mut CreateMessage<'b> {
|
||||
m.content(
|
||||
|
@ -180,14 +257,13 @@ fn handle_beatmap<'a, 'b>(
|
|||
.push_mono_safe(link)
|
||||
.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>(
|
||||
beatmaps: Vec<Beatmap>,
|
||||
link: &'_ str,
|
||||
mode: Option<Mode>,
|
||||
mods: Option<&'_ str>,
|
||||
m: &'a mut CreateMessage<'b>,
|
||||
) -> &'a mut CreateMessage<'b> {
|
||||
let mut beatmaps = beatmaps;
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
use crate::{
|
||||
models::{Beatmap, Mode, Score, User},
|
||||
discord::beatmap_cache::BeatmapMetaCache,
|
||||
discord::oppai_cache::BeatmapCache,
|
||||
models::{Beatmap, Mode, Mods, Score, User},
|
||||
request::{BeatmapRequestKind, UserID},
|
||||
Client as OsuHttpClient,
|
||||
};
|
||||
|
@ -16,10 +18,12 @@ use std::str::FromStr;
|
|||
use youmubot_prelude::*;
|
||||
|
||||
mod announcer;
|
||||
pub(crate) mod beatmap_cache;
|
||||
mod cache;
|
||||
mod db;
|
||||
pub(crate) mod embeds;
|
||||
mod hook;
|
||||
pub(crate) mod oppai_cache;
|
||||
mod server_rank;
|
||||
|
||||
use db::OsuUser;
|
||||
|
@ -57,9 +61,14 @@ pub fn setup(
|
|||
|
||||
// API client
|
||||
let http_client = data.get_cloned::<HTTPClient>();
|
||||
data.insert::<OsuClient>(OsuHttpClient::new(
|
||||
http_client,
|
||||
let osu_client = OsuHttpClient::new(
|
||||
http_client.clone(),
|
||||
std::env::var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."),
|
||||
);
|
||||
data.insert::<OsuClient>(osu_client.clone());
|
||||
data.insert::<oppai_cache::BeatmapCache>(oppai_cache::BeatmapCache::new(http_client));
|
||||
data.insert::<beatmap_cache::BeatmapMetaCache>(beatmap_cache::BeatmapMetaCache::new(
|
||||
osu_client,
|
||||
));
|
||||
|
||||
// Announcer
|
||||
|
@ -171,11 +180,11 @@ struct ModeArg(Mode);
|
|||
impl FromStr for ModeArg {
|
||||
type Err = String;
|
||||
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,
|
||||
Ok(ModeArg(match &s.to_lowercase()[..] {
|
||||
"osu" | "std" => Mode::Std,
|
||||
"taiko" | "osu!taiko" => Mode::Taiko,
|
||||
"ctb" | "fruits" | "catch" | "osu!ctb" | "osu!catch" => Mode::Catch,
|
||||
"osu!mania" | "mania" => Mode::Mania,
|
||||
_ => return Err(format!("Unknown mode {}", s)),
|
||||
}))
|
||||
}
|
||||
|
@ -221,7 +230,8 @@ impl FromStr for Nth {
|
|||
|
||||
fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> CommandResult {
|
||||
let watcher = ctx.data.get_cloned::<ReactionWatcher>();
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
let osu = ctx.data.get_cloned::<BeatmapMetaCache>();
|
||||
let beatmap_cache = ctx.data.get_cloned::<BeatmapCache>();
|
||||
|
||||
if plays.is_empty() {
|
||||
m.reply(&ctx, "No plays found")?;
|
||||
|
@ -232,53 +242,137 @@ fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> Command
|
|||
|
||||
const ITEMS_PER_PAGE: usize = 5;
|
||||
let total_pages = (plays.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
||||
watcher.paginate_fn(ctx, m.channel_id, |page, e| {
|
||||
let page = page as usize;
|
||||
let start = page * ITEMS_PER_PAGE;
|
||||
let end = plays.len().min(start + ITEMS_PER_PAGE);
|
||||
if start >= end {
|
||||
return (e, Err(Error::from("No more pages")));
|
||||
}
|
||||
watcher.paginate_fn(
|
||||
ctx,
|
||||
m.channel_id,
|
||||
|page, e| {
|
||||
let page = page as usize;
|
||||
let start = page * ITEMS_PER_PAGE;
|
||||
let end = plays.len().min(start + ITEMS_PER_PAGE);
|
||||
if start >= end {
|
||||
return (e, Err(Error::from("No more pages")));
|
||||
}
|
||||
|
||||
let plays = &plays[start..end];
|
||||
let beatmaps = {
|
||||
let b = &mut beatmaps[start..end];
|
||||
b.par_iter_mut().enumerate().map(
|
||||
|(i, v)| v.get_or_insert_with(
|
||||
|| osu.beatmaps(BeatmapRequestKind::Beatmap(plays[i].beatmap_id), |f| f)
|
||||
.ok()
|
||||
.and_then(|v| v.into_iter().next())
|
||||
.map(|b| format!(
|
||||
"[{:.1}*] {} - {} [{}] (#{})",
|
||||
b.difficulty.stars, b.artist, b.title, b.difficulty_name, b.beatmap_id))
|
||||
.unwrap_or("FETCH FAILED".to_owned()))).collect::<Vec<_>>()
|
||||
};
|
||||
let /*mods width*/ mw = plays.iter().map(|v| v.mods.to_string().len()).max().unwrap().max(4);
|
||||
let /*beatmap names*/ bw = beatmaps.iter().map(|v| v.len()).max().unwrap().max(7);
|
||||
let plays = &plays[start..end];
|
||||
let beatmaps = {
|
||||
let b = &mut beatmaps[start..end];
|
||||
b.par_iter_mut()
|
||||
.enumerate()
|
||||
.map(|(i, v)| {
|
||||
v.get_or_insert_with(|| {
|
||||
if let Some(b) = osu.get_beatmap(plays[i].beatmap_id, mode).ok() {
|
||||
let stars = beatmap_cache
|
||||
.get_beatmap(b.beatmap_id)
|
||||
.ok()
|
||||
.and_then(|b| {
|
||||
mode.to_oppai_mode().and_then(|mode| {
|
||||
b.get_info_with(Some(mode), plays[i].mods).ok()
|
||||
})
|
||||
})
|
||||
.map(|info| info.stars as f64)
|
||||
.unwrap_or(b.difficulty.stars);
|
||||
format!(
|
||||
"[{:.1}*] {} - {} [{}] ({})",
|
||||
stars,
|
||||
b.artist,
|
||||
b.title,
|
||||
b.difficulty_name,
|
||||
b.short_link(Some(mode), Some(plays[i].mods)),
|
||||
)
|
||||
} else {
|
||||
"FETCH_FAILED".to_owned()
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let pp = plays
|
||||
.iter()
|
||||
.map(|p| {
|
||||
p.pp.map(|pp| format!("{:.2}pp", pp))
|
||||
.or_else(|| {
|
||||
beatmap_cache.get_beatmap(p.beatmap_id).ok().and_then(|b| {
|
||||
mode.to_oppai_mode().and_then(|op| {
|
||||
b.get_pp_from(
|
||||
oppai_rs::Combo::NonFC {
|
||||
max_combo: p.max_combo as u32,
|
||||
misses: p.count_miss as u32,
|
||||
},
|
||||
p.accuracy(mode) as f32,
|
||||
Some(op),
|
||||
p.mods,
|
||||
)
|
||||
.ok()
|
||||
.map(|pp| format!("{:.2}pp [?]", pp))
|
||||
})
|
||||
})
|
||||
})
|
||||
.unwrap_or("-".to_owned())
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let pw = pp.iter().map(|v| v.len()).max().unwrap_or(2);
|
||||
/*mods width*/
|
||||
let mw = plays
|
||||
.iter()
|
||||
.map(|v| v.mods.to_string().len())
|
||||
.max()
|
||||
.unwrap()
|
||||
.max(4);
|
||||
/*beatmap names*/
|
||||
let bw = beatmaps.iter().map(|v| v.len()).max().unwrap().max(7);
|
||||
|
||||
let mut m = MessageBuilder::new();
|
||||
// Table header
|
||||
m.push_line(format!(" # | pp | accuracy | rank | {:mw$} | {:bw$}", "mods", "beatmap", mw = mw, bw = bw));
|
||||
m.push_line(format!("---------------------------------{:-<mw$}---{:-<bw$}", "", "", mw = mw, bw = bw));
|
||||
// Each row
|
||||
for (id, (play, beatmap)) in plays.iter().zip(beatmaps.iter()).enumerate() {
|
||||
m.push_line(
|
||||
format!(
|
||||
"{:>3} | {:>6} | {:>8} | {:^4} | {:mw$} | {:bw$}",
|
||||
let mut m = MessageBuilder::new();
|
||||
// Table header
|
||||
m.push_line(format!(
|
||||
" # | {:pw$} | accuracy | rank | {:mw$} | {:bw$}",
|
||||
"pp",
|
||||
"mods",
|
||||
"beatmap",
|
||||
pw = pw,
|
||||
mw = mw,
|
||||
bw = bw
|
||||
));
|
||||
m.push_line(format!(
|
||||
"------{:-<pw$}---------------------{:-<mw$}---{:-<bw$}",
|
||||
"",
|
||||
"",
|
||||
"",
|
||||
pw = pw,
|
||||
mw = mw,
|
||||
bw = bw
|
||||
));
|
||||
// Each row
|
||||
for (id, (play, beatmap)) in plays.iter().zip(beatmaps.iter()).enumerate() {
|
||||
m.push_line(format!(
|
||||
"{:>3} | {:>pw$} | {:>8} | {:^4} | {:mw$} | {:bw$}",
|
||||
id + start + 1,
|
||||
play.pp.map(|v| format!("{:.2}", v)).unwrap_or("-".to_owned()),
|
||||
pp[id],
|
||||
format!("{:.2}%", play.accuracy(mode)),
|
||||
play.rank.to_string(), play.mods.to_string(), beatmap, mw = mw, bw = bw));
|
||||
}
|
||||
// End
|
||||
let table = m.build().replace("```", "\\`\\`\\`");
|
||||
let mut m = MessageBuilder::new();
|
||||
m
|
||||
.push_codeblock(table, None)
|
||||
.push_line(format!("Page **{}/{}**", page + 1, total_pages))
|
||||
.push_line("Note: star difficulty don't reflect mods applied.");
|
||||
(e.content(m.build()), Ok(()))
|
||||
}, std::time::Duration::from_secs(60))
|
||||
play.rank.to_string(),
|
||||
play.mods.to_string(),
|
||||
beatmap,
|
||||
pw = pw,
|
||||
mw = mw,
|
||||
bw = bw
|
||||
));
|
||||
}
|
||||
// End
|
||||
let table = m.build().replace("```", "\\`\\`\\`");
|
||||
let mut m = MessageBuilder::new();
|
||||
m.push_codeblock(table, None).push_line(format!(
|
||||
"Page **{}/{}**",
|
||||
page + 1,
|
||||
total_pages
|
||||
));
|
||||
if let None = mode.to_oppai_mode() {
|
||||
m.push_line("Note: star difficulty doesn't reflect mods applied.");
|
||||
} else {
|
||||
m.push_line("[?] means pp was predicted by oppai-rs.");
|
||||
}
|
||||
(e.content(m.build()), Ok(()))
|
||||
},
|
||||
std::time::Duration::from_secs(60),
|
||||
)
|
||||
}
|
||||
|
||||
#[command]
|
||||
|
@ -292,6 +386,8 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
let meta_cache = ctx.data.get_cloned::<BeatmapMetaCache>();
|
||||
let oppai = ctx.data.get_cloned::<BeatmapCache>();
|
||||
let user = osu
|
||||
.user(user, |f| f.mode(mode))?
|
||||
.ok_or(Error::from("User not found"))?;
|
||||
|
@ -302,25 +398,22 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
.into_iter()
|
||||
.last()
|
||||
.ok_or(Error::from("No such play"))?;
|
||||
let beatmap = osu
|
||||
.beatmaps(BeatmapRequestKind::Beatmap(recent_play.beatmap_id), |f| {
|
||||
f.mode(mode, true)
|
||||
})?
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|v| BeatmapWithMode(v, mode))
|
||||
let beatmap = meta_cache
|
||||
.get_beatmap(recent_play.beatmap_id, mode)
|
||||
.unwrap();
|
||||
let content = oppai.get_beatmap(beatmap.beatmap_id)?;
|
||||
let beatmap_mode = BeatmapWithMode(beatmap, mode);
|
||||
|
||||
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, None, m))
|
||||
.embed(|m| score_embed(&recent_play, &beatmap_mode, &content, &user, None, m))
|
||||
})?;
|
||||
|
||||
// Save the beatmap...
|
||||
cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &beatmap)?;
|
||||
cache::save_beatmap(&*ctx.data.read(), msg.channel_id, &beatmap_mode)?;
|
||||
}
|
||||
Nth::All => {
|
||||
let plays = osu.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))?;
|
||||
|
@ -332,18 +425,26 @@ pub fn recent(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
|
||||
#[command]
|
||||
#[description = "Show information from the last queried beatmap."]
|
||||
#[num_args(0)]
|
||||
pub fn last(ctx: &mut Context, msg: &Message, _: Args) -> CommandResult {
|
||||
#[usage = "[mods = no mod]"]
|
||||
#[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)?;
|
||||
|
||||
match b {
|
||||
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| {
|
||||
f.content(format!(
|
||||
"{}: here is the beatmap you requested!",
|
||||
msg.author
|
||||
))
|
||||
.embed(|c| beatmap_embed(&b, m, c))
|
||||
.embed(|c| beatmap_embed(&b, m, mods, info, c))
|
||||
})?;
|
||||
}
|
||||
None => {
|
||||
|
@ -371,6 +472,9 @@ pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
let oppai = ctx.data.get_cloned::<BeatmapCache>();
|
||||
|
||||
let content = oppai.get_beatmap(b.beatmap_id)?;
|
||||
|
||||
let user = osu
|
||||
.user(user, |f| f)?
|
||||
|
@ -383,7 +487,7 @@ pub fn check(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult
|
|||
|
||||
for score in scores.into_iter() {
|
||||
msg.channel_id.send_message(&ctx, |c| {
|
||||
c.embed(|m| score_embed(&score, &bm, &user, None, m))
|
||||
c.embed(|m| score_embed(&score, &bm, &content, &user, None, m))
|
||||
})?;
|
||||
}
|
||||
}
|
||||
|
@ -407,6 +511,7 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
let oppai = ctx.data.get_cloned::<BeatmapCache>();
|
||||
let user = osu
|
||||
.user(user, |f| f.mode(mode))?
|
||||
.ok_or(Error::from("User not found"))?;
|
||||
|
@ -427,15 +532,16 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
})?
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|v| BeatmapWithMode(v, mode))
|
||||
.unwrap();
|
||||
let content = oppai.get_beatmap(beatmap.beatmap_id)?;
|
||||
let beatmap = BeatmapWithMode(beatmap, mode);
|
||||
|
||||
msg.channel_id.send_message(&ctx, |m| {
|
||||
m.content(format!(
|
||||
"{}: here is the play that you requested",
|
||||
msg.author
|
||||
))
|
||||
.embed(|m| score_embed(&top_play, &beatmap, &user, Some(rank), m))
|
||||
.embed(|m| score_embed(&top_play, &beatmap, &content, &user, Some(rank), m))
|
||||
})?;
|
||||
|
||||
// Save the beatmap...
|
||||
|
@ -452,18 +558,26 @@ pub fn top(ctx: &mut Context, msg: &Message, mut args: Args) -> CommandResult {
|
|||
fn get_user(ctx: &mut Context, msg: &Message, mut args: Args, mode: Mode) -> CommandResult {
|
||||
let user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||
let cache = ctx.data.get_cloned::<BeatmapMetaCache>();
|
||||
let user = osu.user(user, |f| f.mode(mode))?;
|
||||
let oppai = ctx.data.get_cloned::<BeatmapCache>();
|
||||
match user {
|
||||
Some(u) => {
|
||||
let best = osu
|
||||
.user_best(UserID::ID(u.id), |f| f.limit(1).mode(mode))?
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(|m| {
|
||||
osu.beatmaps(BeatmapRequestKind::Beatmap(m.beatmap_id), |f| {
|
||||
f.mode(mode, true)
|
||||
})
|
||||
.map(|map| (m, BeatmapWithMode(map.into_iter().next().unwrap(), mode)))
|
||||
.map(|m| -> Result<_, Error> {
|
||||
let beatmap = cache.get_beatmap(m.beatmap_id, mode)?;
|
||||
let info = mode
|
||||
.to_oppai_mode()
|
||||
.map(|mode| -> Result<_, Error> {
|
||||
Ok(oppai
|
||||
.get_beatmap(m.beatmap_id)?
|
||||
.get_info_with(Some(mode), m.mods)?)
|
||||
})
|
||||
.transpose()?;
|
||||
Ok((m, BeatmapWithMode(beatmap, mode), info))
|
||||
})
|
||||
.transpose()?;
|
||||
msg.channel_id.send_message(&ctx, |m| {
|
||||
|
|
97
youmubot-osu/src/discord/oppai_cache.rs
Normal file
97
youmubot-osu/src/discord/oppai_cache.rs
Normal file
|
@ -0,0 +1,97 @@
|
|||
use serenity::framework::standard::CommandError;
|
||||
use std::{ffi::CString, sync::Arc};
|
||||
use youmubot_prelude::TypeMapKey;
|
||||
|
||||
/// the information collected from a download/Oppai request.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BeatmapContent {
|
||||
id: u64,
|
||||
content: Arc<CString>,
|
||||
}
|
||||
|
||||
/// the output of "one" oppai run.
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub struct BeatmapInfo {
|
||||
pub stars: f32,
|
||||
pub pp: [f32; 4], // 95, 98, 99, 100
|
||||
}
|
||||
|
||||
impl BeatmapContent {
|
||||
/// Get pp given the combo and accuracy.
|
||||
pub fn get_pp_from(
|
||||
&self,
|
||||
combo: oppai_rs::Combo,
|
||||
accuracy: f32,
|
||||
mode: Option<oppai_rs::Mode>,
|
||||
mods: impl Into<oppai_rs::Mods>,
|
||||
) -> Result<f32, CommandError> {
|
||||
let mut oppai = oppai_rs::Oppai::new_from_content(&self.content[..])?;
|
||||
oppai.combo(combo)?.accuracy(accuracy)?.mods(mods.into());
|
||||
if let Some(mode) = mode {
|
||||
oppai.mode(mode)?;
|
||||
}
|
||||
Ok(oppai.pp())
|
||||
}
|
||||
|
||||
/// Get info given mods.
|
||||
pub fn get_info_with(
|
||||
&self,
|
||||
mode: Option<oppai_rs::Mode>,
|
||||
mods: impl Into<oppai_rs::Mods>,
|
||||
) -> Result<BeatmapInfo, CommandError> {
|
||||
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)?;
|
||||
let pp = [
|
||||
oppai.accuracy(95.0)?.pp(),
|
||||
oppai.accuracy(98.0)?.pp(),
|
||||
oppai.accuracy(99.0)?.pp(),
|
||||
oppai.accuracy(100.0)?.pp(),
|
||||
];
|
||||
let stars = oppai.stars();
|
||||
Ok(BeatmapInfo { stars, pp })
|
||||
}
|
||||
}
|
||||
|
||||
/// A central cache for the beatmaps.
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BeatmapCache {
|
||||
client: reqwest::blocking::Client,
|
||||
cache: Arc<dashmap::DashMap<u64, BeatmapContent>>,
|
||||
}
|
||||
|
||||
impl BeatmapCache {
|
||||
/// Create a new cache.
|
||||
pub fn new(client: reqwest::blocking::Client) -> Self {
|
||||
BeatmapCache {
|
||||
client,
|
||||
cache: Arc::new(dashmap::DashMap::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn download_beatmap(&self, id: u64) -> Result<BeatmapContent, CommandError> {
|
||||
let content = self
|
||||
.client
|
||||
.get(&format!("https://osu.ppy.sh/osu/{}", id))
|
||||
.send()?
|
||||
.bytes()?;
|
||||
Ok(BeatmapContent {
|
||||
id,
|
||||
content: Arc::new(CString::new(content.into_iter().collect::<Vec<_>>())?),
|
||||
})
|
||||
}
|
||||
|
||||
/// Get a beatmap from the cache.
|
||||
pub fn get_beatmap(&self, id: u64) -> Result<BeatmapContent, CommandError> {
|
||||
self.cache
|
||||
.entry(id)
|
||||
.or_try_insert_with(|| self.download_beatmap(id))
|
||||
.map(|v| v.clone())
|
||||
}
|
||||
}
|
||||
|
||||
impl TypeMapKey for BeatmapCache {
|
||||
type Value = BeatmapCache;
|
||||
}
|
|
@ -50,17 +50,19 @@ pub fn server_rank(ctx: &mut Context, m: &Message, mut args: Args) -> CommandRes
|
|||
m.channel_id,
|
||||
move |page: u8, e: &mut EditMessage| {
|
||||
let start = (page as usize) * ITEMS_PER_PAGE;
|
||||
if start >= users.len() {
|
||||
let end = (start + ITEMS_PER_PAGE).min(users.len());
|
||||
if start >= end {
|
||||
return (e, Err(Error("No more items".to_owned())));
|
||||
}
|
||||
let total_len = users.len();
|
||||
let users = users.iter().skip(start).take(ITEMS_PER_PAGE);
|
||||
let users = &users[start..end];
|
||||
let username_len = users.iter().map(|(_, u)| u.len()).max().unwrap_or(8).max(8);
|
||||
let mut content = MessageBuilder::new();
|
||||
content
|
||||
.push_line("```")
|
||||
.push_line("Rank | pp | Username")
|
||||
.push_line("-------------------------");
|
||||
for (id, (pp, member)) in users.enumerate() {
|
||||
.push_line(format!("-----------------{:-<uw$}", "", uw = username_len));
|
||||
for (id, (pp, member)) in users.iter().enumerate() {
|
||||
content
|
||||
.push(format!(
|
||||
"{:>4} | {:>7.2} | ",
|
||||
|
|
|
@ -14,7 +14,7 @@ use std::{convert::TryInto, sync::Arc};
|
|||
|
||||
/// Client is the client that will perform calls to the osu! api server.
|
||||
/// It's cheap to clone, so do it.
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Client {
|
||||
key: Arc<String>,
|
||||
client: HTTPClient,
|
||||
|
|
|
@ -45,6 +45,77 @@ pub struct Difficulty {
|
|||
pub count_slider: u64,
|
||||
pub count_spinner: u64,
|
||||
pub max_combo: Option<u64>,
|
||||
|
||||
pub drain_length: Duration,
|
||||
pub total_length: Duration,
|
||||
}
|
||||
|
||||
impl Difficulty {
|
||||
// Difficulty calculation is based on
|
||||
// https://www.reddit.com/r/osugame/comments/6phntt/difficulty_settings_table_with_all_values/
|
||||
|
||||
fn apply_everything_by_ratio(&mut self, rat: f64) {
|
||||
self.cs = (self.cs * rat).min(10.0);
|
||||
self.od = (self.od * rat).min(10.0);
|
||||
self.ar = (self.ar * rat).min(10.0);
|
||||
self.hp = (self.hp * rat).min(10.0);
|
||||
}
|
||||
fn apply_ar_by_time_ratio(&mut self, rat: f64) {
|
||||
// Convert AR to approach time...
|
||||
let approach_time = if self.ar < 5.0 {
|
||||
1800.0 - self.ar * 120.0
|
||||
} else {
|
||||
1200.0 - (self.ar - 5.0) * 150.0
|
||||
};
|
||||
// Update it...
|
||||
let approach_time = approach_time * rat;
|
||||
// Convert it back to AR...
|
||||
self.ar = if approach_time > 1200.0 {
|
||||
(1800.0 - approach_time) / 120.0
|
||||
} else {
|
||||
(1200.0 - approach_time) / 150.0 + 5.0
|
||||
};
|
||||
}
|
||||
fn apply_od_by_time_ratio(&mut self, rat: f64) {
|
||||
// Convert OD to hit timing
|
||||
let hit_timing = 79.0 - self.od * 6.0 + 0.5;
|
||||
// Update it...
|
||||
let hit_timing = hit_timing * rat + 0.5 / rat;
|
||||
// then convert back
|
||||
self.od = (79.0 - (hit_timing - 0.5)) / 6.0;
|
||||
}
|
||||
fn apply_length_by_ratio(&mut self, mul: u32, div: u32) {
|
||||
self.drain_length = self.drain_length * mul / div;
|
||||
self.total_length = self.total_length * mul / div;
|
||||
}
|
||||
/// Apply mods to the given difficulty.
|
||||
/// Note that `stars`, `aim` and `speed` cannot be calculated from this alone.
|
||||
pub fn apply_mods(&self, mods: Mods) -> Difficulty {
|
||||
let mut diff = self.clone();
|
||||
|
||||
// Apply mods one by one
|
||||
if mods.contains(Mods::EZ) {
|
||||
diff.apply_everything_by_ratio(0.5);
|
||||
}
|
||||
if mods.contains(Mods::HT) {
|
||||
diff.apply_ar_by_time_ratio(4.0 / 3.0);
|
||||
diff.apply_od_by_time_ratio(4.0 / 3.0);
|
||||
diff.apply_length_by_ratio(4, 3);
|
||||
}
|
||||
if mods.contains(Mods::HR) {
|
||||
let old_cs = diff.cs;
|
||||
diff.apply_everything_by_ratio(1.4);
|
||||
// CS is changed by 1.3 tho
|
||||
diff.cs = old_cs * 1.3;
|
||||
}
|
||||
if mods.contains(Mods::DT) {
|
||||
diff.apply_ar_by_time_ratio(2.0 / 3.0);
|
||||
diff.apply_od_by_time_ratio(2.0 / 3.0);
|
||||
diff.apply_length_by_ratio(2, 3);
|
||||
}
|
||||
|
||||
diff
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, Debug, Deserialize, Serialize)]
|
||||
|
@ -94,7 +165,7 @@ impl fmt::Display for Language {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize, std::hash::Hash)]
|
||||
pub enum Mode {
|
||||
Std,
|
||||
Taiko,
|
||||
|
@ -118,6 +189,38 @@ 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,
|
||||
})
|
||||
}
|
||||
|
||||
/// Parse from the new site's convention.
|
||||
pub fn parse_from_new_site(s: &str) -> Option<Self> {
|
||||
Some(match s {
|
||||
"osu" => Mode::Std,
|
||||
"taiko" => Mode::Taiko,
|
||||
"fruits" => Mode::Catch,
|
||||
"mania" => Mode::Mania,
|
||||
_ => return None,
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns the mode string in the new convention.
|
||||
pub fn to_str_new_site(&self) -> &'static str {
|
||||
match self {
|
||||
Mode::Std => "osu",
|
||||
Mode::Taiko => "taiko",
|
||||
Mode::Catch => "fruits",
|
||||
Mode::Mania => "mania",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||
pub struct Beatmap {
|
||||
// Beatmapset info
|
||||
|
@ -141,8 +244,6 @@ pub struct Beatmap {
|
|||
pub beatmap_id: u64,
|
||||
pub difficulty_name: String,
|
||||
pub difficulty: Difficulty,
|
||||
pub drain_length: Duration,
|
||||
pub total_length: Duration,
|
||||
pub file_hash: String,
|
||||
pub mode: Mode,
|
||||
pub favourite_count: u64,
|
||||
|
@ -169,6 +270,19 @@ impl Beatmap {
|
|||
)
|
||||
}
|
||||
|
||||
/// Return a parsable short link.
|
||||
pub fn short_link(&self, override_mode: Option<Mode>, mods: Option<Mods>) -> String {
|
||||
format!(
|
||||
"/b/{}{}{}",
|
||||
self.beatmap_id,
|
||||
match override_mode {
|
||||
Some(mode) if mode != self.mode => format!("/{}", mode.to_str_new_site()),
|
||||
_ => "".to_owned(),
|
||||
},
|
||||
mods.map(|m| format!("{}", m)).unwrap_or("".to_owned()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Link to the cover image of the beatmap.
|
||||
pub fn cover_url(&self) -> String {
|
||||
format!(
|
||||
|
|
|
@ -128,3 +128,9 @@ impl fmt::Display for Mods {
|
|||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Mods> for oppai_rs::Mods {
|
||||
fn from(m: Mods) -> Self {
|
||||
oppai_rs::Mods::from_bits_truncate(m.bits() as i32)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -139,9 +139,9 @@ impl TryFrom<raw::Beatmap> for Beatmap {
|
|||
count_slider: parse_from_str(&raw.count_slider)?,
|
||||
count_spinner: parse_from_str(&raw.count_spinner)?,
|
||||
max_combo: raw.max_combo.map(parse_from_str).transpose()?,
|
||||
drain_length: parse_duration(&raw.hit_length)?,
|
||||
total_length: parse_duration(&raw.total_length)?,
|
||||
},
|
||||
drain_length: parse_duration(&raw.hit_length)?,
|
||||
total_length: parse_duration(&raw.total_length)?,
|
||||
file_hash: raw.file_md5,
|
||||
mode: parse_mode(&raw.mode)?,
|
||||
favourite_count: parse_from_str(&raw.favourite_count)?,
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Deserialize, Clone, Debug)]
|
||||
pub(crate) struct Beatmap {
|
||||
pub approved: String,
|
||||
pub submit_date: String,
|
||||
|
|
|
@ -168,6 +168,51 @@ impl AnnouncerHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gets the announcer of the given guild.
|
||||
pub fn announcer_of(
|
||||
ctx: &Context,
|
||||
key: &'static str,
|
||||
guild: GuildId,
|
||||
) -> Result<Option<ChannelId>, Error> {
|
||||
Ok(AnnouncerChannels::open(&*ctx.data.read())
|
||||
.borrow()?
|
||||
.get(key)
|
||||
.and_then(|channels| channels.get(&guild).cloned()))
|
||||
}
|
||||
|
||||
#[command("list")]
|
||||
#[description = "List the registered announcers of this server"]
|
||||
#[num_args(0)]
|
||||
#[only_in(guilds)]
|
||||
pub fn list_announcers(ctx: &mut Context, m: &Message, _: Args) -> CommandResult {
|
||||
let guild_id = m.guild_id.unwrap();
|
||||
let announcers = AnnouncerChannels::open(&*ctx.data.read());
|
||||
let announcers = announcers.borrow()?;
|
||||
|
||||
let channels = ctx
|
||||
.data
|
||||
.get_cloned::<AnnouncerHandler>()
|
||||
.into_iter()
|
||||
.filter_map(|key| {
|
||||
announcers
|
||||
.get(key)
|
||||
.and_then(|channels| channels.get(&guild_id))
|
||||
.map(|&ch| (key, ch))
|
||||
})
|
||||
.map(|(key, ch)| format!(" - `{}`: activated on channel {}", key, ch.mention()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
m.reply(
|
||||
&ctx,
|
||||
format!(
|
||||
"Activated announcers on this server:\n{}",
|
||||
channels.join("\n")
|
||||
),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[command("register")]
|
||||
#[description = "Register the current channel with an announcer"]
|
||||
#[usage = "[announcer key]"]
|
||||
|
@ -253,5 +298,5 @@ pub fn remove_announcer(ctx: &mut Context, m: &Message, mut args: Args) -> Comma
|
|||
#[only_in(guilds)]
|
||||
#[required_permissions(MANAGE_CHANNELS)]
|
||||
#[description = "Manage the announcers in the server."]
|
||||
#[commands(remove_announcer, register_announcer)]
|
||||
#[commands(remove_announcer, register_announcer, list_announcers)]
|
||||
pub struct AnnouncerCommands;
|
||||
|
|
Loading…
Add table
Reference in a new issue