mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-20 09:18: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:
|
steps:
|
||||||
- name: format_check
|
- name: format_check
|
||||||
image: rust:1.41
|
image: rust:1.44
|
||||||
commands:
|
commands:
|
||||||
- rustup component add rustfmt
|
- rustup component add rustfmt
|
||||||
- cargo fmt -- --check
|
- cargo fmt -- --check
|
||||||
- name: cargo_check
|
- name: cargo_check
|
||||||
image: rust:1.41
|
image: rust:1.44
|
||||||
commands:
|
commands:
|
||||||
- cargo check
|
- cargo check
|
||||||
|
|
||||||
|
@ -31,7 +31,7 @@ trigger:
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: build_release
|
- name: build_release
|
||||||
image: rust:1.41
|
image: rust:1.44
|
||||||
commands:
|
commands:
|
||||||
- cargo build --release
|
- cargo build --release
|
||||||
- name: deploy
|
- name: deploy
|
||||||
|
|
53
Cargo.lock
generated
53
Cargo.lock
generated
|
@ -22,6 +22,14 @@ name = "adler32"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
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]]
|
[[package]]
|
||||||
name = "aho-corasick"
|
name = "aho-corasick"
|
||||||
version = "0.7.10"
|
version = "0.7.10"
|
||||||
|
@ -162,6 +170,24 @@ dependencies = [
|
||||||
"syn 1.0.23 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "core-foundation"
|
name = "core-foundation"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@ -244,6 +270,16 @@ dependencies = [
|
||||||
"sct 0.6.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "digest"
|
name = "digest"
|
||||||
version = "0.8.1"
|
version = "0.8.1"
|
||||||
|
@ -796,6 +832,16 @@ dependencies = [
|
||||||
"vcpkg 0.2.8 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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]]
|
[[package]]
|
||||||
name = "parking_lot"
|
name = "parking_lot"
|
||||||
version = "0.9.0"
|
version = "0.9.0"
|
||||||
|
@ -1710,7 +1756,9 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 1.2.1 (registry+https://github.com/rust-lang/crates.io-index)",
|
"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)",
|
"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)",
|
"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)",
|
"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)",
|
"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)",
|
"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 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 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 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 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 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"
|
"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 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 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 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 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 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"
|
"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-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 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 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 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 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"
|
"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 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-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 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 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 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"
|
"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"
|
rayon = "1.1"
|
||||||
lazy_static = "1"
|
lazy_static = "1"
|
||||||
regex = "1"
|
regex = "1"
|
||||||
|
oppai-rs = "0.2.0"
|
||||||
|
dashmap = "3.11.4"
|
||||||
|
|
||||||
youmubot-db = { path = "../youmubot-db" }
|
youmubot-db = { path = "../youmubot-db" }
|
||||||
youmubot-prelude = { path = "../youmubot-prelude" }
|
youmubot-prelude = { path = "../youmubot-prelude" }
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
use super::db::{OsuSavedUsers, OsuUser};
|
use super::db::{OsuSavedUsers, OsuUser};
|
||||||
use super::{embeds::score_embed, BeatmapWithMode, OsuClient};
|
use super::{embeds::score_embed, BeatmapWithMode, OsuClient};
|
||||||
use crate::{
|
use crate::{
|
||||||
|
discord::beatmap_cache::BeatmapMetaCache,
|
||||||
|
discord::oppai_cache::BeatmapCache,
|
||||||
models::{Mode, Score},
|
models::{Mode, Score},
|
||||||
request::{BeatmapRequestKind, UserID},
|
request::UserID,
|
||||||
Client as Osu,
|
Client as Osu,
|
||||||
};
|
};
|
||||||
use announcer::MemberToChannels;
|
use announcer::MemberToChannels;
|
||||||
|
@ -22,6 +24,8 @@ pub const ANNOUNCER_KEY: &'static str = "osu";
|
||||||
/// Announce osu! top scores.
|
/// Announce osu! top scores.
|
||||||
pub fn updates(c: Arc<CacheAndHttp>, d: AppData, channels: MemberToChannels) -> CommandResult {
|
pub fn updates(c: Arc<CacheAndHttp>, d: AppData, channels: MemberToChannels) -> CommandResult {
|
||||||
let osu = d.get_cloned::<OsuClient>();
|
let osu = d.get_cloned::<OsuClient>();
|
||||||
|
let cache = d.get_cloned::<BeatmapMetaCache>();
|
||||||
|
let oppai = d.get_cloned::<BeatmapCache>();
|
||||||
// For each user...
|
// For each user...
|
||||||
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
let mut data = OsuSavedUsers::open(&*d.read()).borrow()?.clone();
|
||||||
for (user_id, osu_user) in data.iter_mut() {
|
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])
|
osu_user.pp = match (&[Mode::Std, Mode::Taiko, Mode::Catch, Mode::Mania])
|
||||||
.par_iter()
|
.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<_, _>>()
|
.collect::<Result<_, _>>()
|
||||||
{
|
{
|
||||||
Ok(v) => v,
|
Ok(v) => v,
|
||||||
|
@ -51,6 +66,8 @@ pub fn updates(c: Arc<CacheAndHttp>, d: AppData, channels: MemberToChannels) ->
|
||||||
fn handle_user_mode(
|
fn handle_user_mode(
|
||||||
c: Arc<CacheAndHttp>,
|
c: Arc<CacheAndHttp>,
|
||||||
osu: &Osu,
|
osu: &Osu,
|
||||||
|
cache: &BeatmapMetaCache,
|
||||||
|
oppai: &BeatmapCache,
|
||||||
osu_user: &OsuUser,
|
osu_user: &OsuUser,
|
||||||
user_id: UserId,
|
user_id: UserId,
|
||||||
channels: &[ChannelId],
|
channels: &[ChannelId],
|
||||||
|
@ -62,23 +79,17 @@ fn handle_user_mode(
|
||||||
.ok_or(Error::from("user not found"))?;
|
.ok_or(Error::from("user not found"))?;
|
||||||
scores
|
scores
|
||||||
.into_par_iter()
|
.into_par_iter()
|
||||||
.filter_map(|(rank, score)| {
|
.map(|(rank, score)| -> Result<_, Error> {
|
||||||
let beatmap = osu
|
let beatmap = cache.get_beatmap_default(score.beatmap_id)?;
|
||||||
.beatmaps(BeatmapRequestKind::Beatmap(score.beatmap_id), |f| f)
|
let content = oppai.get_beatmap(beatmap.beatmap_id)?;
|
||||||
.map(|v| BeatmapWithMode(v.into_iter().next().unwrap(), mode));
|
Ok((rank, score, BeatmapWithMode(beatmap, mode), content))
|
||||||
match beatmap {
|
|
||||||
Ok(v) => Some((rank, score, v)),
|
|
||||||
Err(e) => {
|
|
||||||
dbg!(e);
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.for_each(|(rank, score, beatmap)| {
|
.filter_map(|v| v.ok())
|
||||||
|
.for_each(|(rank, score, beatmap, content)| {
|
||||||
for channel in (&channels).iter() {
|
for channel in (&channels).iter() {
|
||||||
if let Err(e) = channel.send_message(c.http(), |c| {
|
if let Err(e) = channel.send_message(c.http(), |c| {
|
||||||
c.content(format!("New top record from {}!", user_id.mention()))
|
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);
|
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 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 chrono::Utc;
|
||||||
use serenity::{builder::CreateEmbed, utils::MessageBuilder};
|
use serenity::{builder::CreateEmbed, utils::MessageBuilder};
|
||||||
use youmubot_prelude::*;
|
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(
|
c.title(
|
||||||
MessageBuilder::new()
|
MessageBuilder::new()
|
||||||
.push_bold_safe(&b.artist)
|
.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(" [")
|
||||||
.push_bold_safe(&b.difficulty_name)
|
.push_bold_safe(&b.difficulty_name)
|
||||||
.push("]")
|
.push("]")
|
||||||
|
.push(&mod_str)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.author(|a| {
|
.author(|a| {
|
||||||
|
@ -34,27 +50,37 @@ 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)
|
||||||
|
),
|
||||||
|
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,
|
false,
|
||||||
)
|
)
|
||||||
|
}))
|
||||||
.field(
|
.field(
|
||||||
"Length",
|
"Length",
|
||||||
MessageBuilder::new()
|
MessageBuilder::new()
|
||||||
.push_bold_safe(Duration(b.total_length))
|
.push_bold_safe(Duration(diff.total_length))
|
||||||
.push(" (")
|
.push(" (")
|
||||||
.push_bold_safe(Duration(b.drain_length))
|
.push_bold_safe(Duration(diff.drain_length))
|
||||||
.push(" drain)")
|
.push(" drain)")
|
||||||
.build(),
|
.build(),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.field("Circle Size", format!("{:.1}", b.difficulty.cs), true)
|
.field("Circle Size", format!("{:.1}", diff.cs), true)
|
||||||
.field("Approach Rate", format!("{:.1}", b.difficulty.ar), true)
|
.field("Approach Rate", format!("{:.1}", diff.ar), true)
|
||||||
.field(
|
.field("Overall Difficulty", format!("{:.1}", diff.od), true)
|
||||||
"Overall Difficulty",
|
.field("HP Drain", format!("{:.1}", diff.hp), true)
|
||||||
format!("{:.1}", b.difficulty.od),
|
|
||||||
true,
|
|
||||||
)
|
|
||||||
.field("HP Drain", format!("{:.1}", b.difficulty.hp), true)
|
|
||||||
.field("BPM", b.bpm.round(), true)
|
.field("BPM", b.bpm.round(), true)
|
||||||
.fields(b.difficulty.max_combo.map(|v| ("Max combo", v, true)))
|
.fields(b.difficulty.max_combo.map(|v| ("Max combo", v, true)))
|
||||||
.field("Mode", format_mode(m, b.mode), 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(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("Language: ")
|
||||||
.push_bold(&b.language)
|
.push_bold(&b.language)
|
||||||
.push(" | Genre: ")
|
.push(" | Genre: ")
|
||||||
.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;
|
||||||
|
@ -145,7 +181,7 @@ pub fn beatmapset_embed<'a>(
|
||||||
.field(
|
.field(
|
||||||
"Length",
|
"Length",
|
||||||
MessageBuilder::new()
|
MessageBuilder::new()
|
||||||
.push_bold_safe(Duration(b.total_length))
|
.push_bold_safe(Duration(b.difficulty.total_length))
|
||||||
.build(),
|
.build(),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
|
@ -177,7 +213,11 @@ pub fn beatmapset_embed<'a>(
|
||||||
(
|
(
|
||||||
format!("[{}]", b.difficulty_name),
|
format!("[{}]", b.difficulty_name),
|
||||||
MessageBuilder::new()
|
MessageBuilder::new()
|
||||||
.push(format!("[[Link]]({})", b.link()))
|
.push(format!(
|
||||||
|
"[[Link]]({}) (`{}`)",
|
||||||
|
b.link(),
|
||||||
|
b.short_link(m, None)
|
||||||
|
))
|
||||||
.push(", ")
|
.push(", ")
|
||||||
.push_bold(format!("{:.2}⭐", b.difficulty.stars))
|
.push_bold(format!("{:.2}⭐", b.difficulty.stars))
|
||||||
.push(", ")
|
.push(", ")
|
||||||
|
@ -191,7 +231,7 @@ pub fn beatmapset_embed<'a>(
|
||||||
.push(", HP")
|
.push(", HP")
|
||||||
.push_bold(format!("{:.1}", b.difficulty.hp))
|
.push_bold(format!("{:.1}", b.difficulty.hp))
|
||||||
.push(", ⌛ ")
|
.push(", ⌛ ")
|
||||||
.push_bold(format!("{}", Duration(b.drain_length)))
|
.push_bold(format!("{}", Duration(b.difficulty.drain_length)))
|
||||||
.build(),
|
.build(),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
@ -201,6 +241,7 @@ pub fn beatmapset_embed<'a>(
|
||||||
pub(crate) fn score_embed<'a>(
|
pub(crate) fn score_embed<'a>(
|
||||||
s: &Score,
|
s: &Score,
|
||||||
bm: &BeatmapWithMode,
|
bm: &BeatmapWithMode,
|
||||||
|
content: &BeatmapContent,
|
||||||
u: &User,
|
u: &User,
|
||||||
top_record: Option<u8>,
|
top_record: Option<u8>,
|
||||||
m: &'a mut CreateEmbed,
|
m: &'a mut CreateEmbed,
|
||||||
|
@ -208,6 +249,11 @@ pub(crate) fn score_embed<'a>(
|
||||||
let mode = bm.mode();
|
let mode = bm.mode();
|
||||||
let b = &bm.0;
|
let b = &bm.0;
|
||||||
let accuracy = s.accuracy(mode);
|
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 {
|
let score_line = match &s.rank {
|
||||||
Rank::SS | Rank::SSH => format!("SS"),
|
Rank::SS | Rank::SSH => format!("SS"),
|
||||||
_ if s.perfect => format!("{:.2}% FC", accuracy),
|
_ 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
|
accuracy, s.max_combo, s.count_miss, v
|
||||||
),
|
),
|
||||||
};
|
};
|
||||||
let score_line =
|
let pp = s.pp.map(|pp| format!("{:.2}pp", pp)).or_else(|| {
|
||||||
s.pp.map(|pp| format!("{} | {:.2}pp", &score_line, pp))
|
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);
|
.unwrap_or(score_line);
|
||||||
let top_record = top_record
|
let top_record = top_record
|
||||||
.map(|v| format!("| #{} top record!", v))
|
.map(|v| format!("| #{} top record!", v))
|
||||||
.unwrap_or("".to_owned());
|
.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()))
|
m.author(|f| f.name(&u.username).url(u.link()).icon_url(u.avatar_url()))
|
||||||
.color(0xffb6c1)
|
.color(0xffb6c1)
|
||||||
.title(format!(
|
.title(format!(
|
||||||
|
@ -232,7 +293,7 @@ pub(crate) fn score_embed<'a>(
|
||||||
b.title,
|
b.title,
|
||||||
b.difficulty_name,
|
b.difficulty_name,
|
||||||
s.mods,
|
s.mods,
|
||||||
b.difficulty.stars,
|
stars,
|
||||||
b.creator,
|
b.creator,
|
||||||
score_line,
|
score_line,
|
||||||
top_record
|
top_record
|
||||||
|
@ -245,15 +306,26 @@ pub(crate) fn score_embed<'a>(
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.field("Rank", &score_line, 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)))
|
.fields(s.pp.map(|pp| ("pp gained", format!("{:.2}pp", pp), true)))
|
||||||
.field("Creator", &b.creator, true)
|
|
||||||
.field("Mode", mode.to_string(), true)
|
.field("Mode", mode.to_string(), true)
|
||||||
.field(
|
.field(
|
||||||
"Map stats",
|
"Map stats",
|
||||||
MessageBuilder::new()
|
MessageBuilder::new()
|
||||||
.push(format!("[[Link]]({})", b.link()))
|
.push(format!(
|
||||||
|
"[[Link]]({}) (`{}`)",
|
||||||
|
b.link(),
|
||||||
|
b.short_link(Some(mode), Some(s.mods))
|
||||||
|
))
|
||||||
.push(", ")
|
.push(", ")
|
||||||
.push_bold(format!("{:.2}⭐", b.difficulty.stars))
|
.push_bold(format!("{:.2}⭐", stars))
|
||||||
.push(", ")
|
.push(", ")
|
||||||
.push_bold_line(
|
.push_bold_line(
|
||||||
b.mode.to_string()
|
b.mode.to_string()
|
||||||
|
@ -264,24 +336,29 @@ pub(crate) fn score_embed<'a>(
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
.push("CS")
|
.push("CS")
|
||||||
.push_bold(format!("{:.1}", b.difficulty.cs))
|
.push_bold(format!("{:.1}", diff.cs))
|
||||||
.push(", AR")
|
.push(", AR")
|
||||||
.push_bold(format!("{:.1}", b.difficulty.ar))
|
.push_bold(format!("{:.1}", diff.ar))
|
||||||
.push(", OD")
|
.push(", OD")
|
||||||
.push_bold(format!("{:.1}", b.difficulty.od))
|
.push_bold(format!("{:.1}", diff.od))
|
||||||
.push(", HP")
|
.push(", HP")
|
||||||
.push_bold(format!("{:.1}", b.difficulty.hp))
|
.push_bold(format!("{:.1}", diff.hp))
|
||||||
.push(", ⌛ ")
|
.push(", ⌛ ")
|
||||||
.push_bold(format!("{}", Duration(b.drain_length)))
|
.push_bold(format!("{}", Duration(diff.drain_length)))
|
||||||
.build(),
|
.build(),
|
||||||
false,
|
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>(
|
pub(crate) fn user_embed<'a>(
|
||||||
u: User,
|
u: User,
|
||||||
best: Option<(Score, BeatmapWithMode)>,
|
best: Option<(Score, BeatmapWithMode, Option<BeatmapInfo>)>,
|
||||||
m: &'a mut CreateEmbed,
|
m: &'a mut CreateEmbed,
|
||||||
) -> &'a mut CreateEmbed {
|
) -> &'a mut CreateEmbed {
|
||||||
m.title(u.username)
|
m.title(u.username)
|
||||||
|
@ -323,8 +400,8 @@ pub(crate) fn user_embed<'a>(
|
||||||
),
|
),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
.fields(best.map(|(v, map)| {
|
.fields(best.map(|(v, map, info)| {
|
||||||
let map = map.0;
|
let BeatmapWithMode(map, mode) = map;
|
||||||
(
|
(
|
||||||
"Best Record",
|
"Best Record",
|
||||||
MessageBuilder::new()
|
MessageBuilder::new()
|
||||||
|
@ -348,8 +425,12 @@ pub(crate) fn user_embed<'a>(
|
||||||
MessageBuilder::new().push_bold_safe(&map.title).build(),
|
MessageBuilder::new().push_bold_safe(&map.title).build(),
|
||||||
map.link()
|
map.link()
|
||||||
))
|
))
|
||||||
.push(format!(" [{}]", map.difficulty_name))
|
.push_line(format!(" [{}]", map.difficulty_name))
|
||||||
.push(format!(" ({:.1}⭐)", map.difficulty.stars))
|
.push(format!(
|
||||||
|
"{:.1}⭐ | `{}`",
|
||||||
|
info.map(|i| i.stars as f64).unwrap_or(map.difficulty.stars),
|
||||||
|
map.short_link(Some(mode), Some(v.mods))
|
||||||
|
))
|
||||||
.build(),
|
.build(),
|
||||||
false,
|
false,
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
use super::OsuClient;
|
use super::OsuClient;
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{Beatmap, Mode},
|
discord::beatmap_cache::BeatmapMetaCache,
|
||||||
|
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 +13,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};
|
||||||
|
@ -22,6 +25,9 @@ lazy_static! {
|
||||||
static ref NEW_LINK_REGEX: Regex = Regex::new(
|
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]+))?"
|
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();
|
).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) -> () {
|
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 mut v = move || -> CommandResult {
|
||||||
let old_links = handle_old_links(ctx, &msg.content)?;
|
let old_links = handle_old_links(ctx, &msg.content)?;
|
||||||
let new_links = handle_new_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;
|
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 {
|
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 +70,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 +78,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 +111,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 +134,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,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> {
|
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
|
||||||
Some(match v.as_str() {
|
.name("mode")
|
||||||
"osu" => Mode::Std,
|
.and_then(|v| Mode::parse_from_new_site(v.as_str()));
|
||||||
"taiko" => Mode::Taiko,
|
|
||||||
"fruits" => Mode::Catch,
|
|
||||||
"mania" => Mode::Mania,
|
|
||||||
_ => 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 +163,23 @@ 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")
|
||||||
|
.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 {
|
to_prints.push(ToPrint {
|
||||||
embed: EmbedType::Beatmap(beatmap),
|
embed: EmbedType::Beatmap(beatmap, info, mods),
|
||||||
link,
|
link,
|
||||||
mods,
|
|
||||||
mode,
|
mode,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -159,7 +187,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,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
@ -167,11 +194,61 @@ fn handle_new_links<'a>(ctx: &mut Context, content: &'a str) -> Result<Vec<ToPri
|
||||||
Ok(to_prints)
|
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>(
|
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 +257,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;
|
||||||
|
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{Beatmap, Mode, Score, User},
|
discord::beatmap_cache::BeatmapMetaCache,
|
||||||
|
discord::oppai_cache::BeatmapCache,
|
||||||
|
models::{Beatmap, Mode, Mods, Score, User},
|
||||||
request::{BeatmapRequestKind, UserID},
|
request::{BeatmapRequestKind, UserID},
|
||||||
Client as OsuHttpClient,
|
Client as OsuHttpClient,
|
||||||
};
|
};
|
||||||
|
@ -16,10 +18,12 @@ use std::str::FromStr;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
mod announcer;
|
mod announcer;
|
||||||
|
pub(crate) mod beatmap_cache;
|
||||||
mod cache;
|
mod cache;
|
||||||
mod db;
|
mod db;
|
||||||
pub(crate) mod embeds;
|
pub(crate) mod embeds;
|
||||||
mod hook;
|
mod hook;
|
||||||
|
pub(crate) mod oppai_cache;
|
||||||
mod server_rank;
|
mod server_rank;
|
||||||
|
|
||||||
use db::OsuUser;
|
use db::OsuUser;
|
||||||
|
@ -57,9 +61,14 @@ pub fn setup(
|
||||||
|
|
||||||
// API client
|
// API client
|
||||||
let http_client = data.get_cloned::<HTTPClient>();
|
let http_client = data.get_cloned::<HTTPClient>();
|
||||||
data.insert::<OsuClient>(OsuHttpClient::new(
|
let osu_client = OsuHttpClient::new(
|
||||||
http_client,
|
http_client.clone(),
|
||||||
std::env::var("OSU_API_KEY").expect("Please set OSU_API_KEY as osu! api key."),
|
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
|
// Announcer
|
||||||
|
@ -171,11 +180,11 @@ struct ModeArg(Mode);
|
||||||
impl FromStr for ModeArg {
|
impl FromStr for ModeArg {
|
||||||
type Err = String;
|
type Err = String;
|
||||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||||
Ok(ModeArg(match s {
|
Ok(ModeArg(match &s.to_lowercase()[..] {
|
||||||
"std" => Mode::Std,
|
"osu" | "std" => Mode::Std,
|
||||||
"taiko" => Mode::Taiko,
|
"taiko" | "osu!taiko" => Mode::Taiko,
|
||||||
"catch" => Mode::Catch,
|
"ctb" | "fruits" | "catch" | "osu!ctb" | "osu!catch" => Mode::Catch,
|
||||||
"mania" => Mode::Mania,
|
"osu!mania" | "mania" => Mode::Mania,
|
||||||
_ => return Err(format!("Unknown mode {}", s)),
|
_ => 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 {
|
fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> CommandResult {
|
||||||
let watcher = ctx.data.get_cloned::<ReactionWatcher>();
|
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() {
|
if plays.is_empty() {
|
||||||
m.reply(&ctx, "No plays found")?;
|
m.reply(&ctx, "No plays found")?;
|
||||||
|
@ -232,7 +242,10 @@ fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> Command
|
||||||
|
|
||||||
const ITEMS_PER_PAGE: usize = 5;
|
const ITEMS_PER_PAGE: usize = 5;
|
||||||
let total_pages = (plays.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
let total_pages = (plays.len() + ITEMS_PER_PAGE - 1) / ITEMS_PER_PAGE;
|
||||||
watcher.paginate_fn(ctx, m.channel_id, |page, e| {
|
watcher.paginate_fn(
|
||||||
|
ctx,
|
||||||
|
m.channel_id,
|
||||||
|
|page, e| {
|
||||||
let page = page as usize;
|
let page = page as usize;
|
||||||
let start = page * ITEMS_PER_PAGE;
|
let start = page * ITEMS_PER_PAGE;
|
||||||
let end = plays.len().min(start + ITEMS_PER_PAGE);
|
let end = plays.len().min(start + ITEMS_PER_PAGE);
|
||||||
|
@ -243,42 +256,123 @@ fn list_plays(plays: &[Score], mode: Mode, ctx: Context, m: &Message) -> Command
|
||||||
let plays = &plays[start..end];
|
let plays = &plays[start..end];
|
||||||
let beatmaps = {
|
let beatmaps = {
|
||||||
let b = &mut beatmaps[start..end];
|
let b = &mut beatmaps[start..end];
|
||||||
b.par_iter_mut().enumerate().map(
|
b.par_iter_mut()
|
||||||
|(i, v)| v.get_or_insert_with(
|
.enumerate()
|
||||||
|| osu.beatmaps(BeatmapRequestKind::Beatmap(plays[i].beatmap_id), |f| f)
|
.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()
|
.ok()
|
||||||
.and_then(|v| v.into_iter().next())
|
.and_then(|b| {
|
||||||
.map(|b| format!(
|
mode.to_oppai_mode().and_then(|mode| {
|
||||||
"[{:.1}*] {} - {} [{}] (#{})",
|
b.get_info_with(Some(mode), plays[i].mods).ok()
|
||||||
b.difficulty.stars, b.artist, b.title, b.difficulty_name, b.beatmap_id))
|
})
|
||||||
.unwrap_or("FETCH FAILED".to_owned()))).collect::<Vec<_>>()
|
})
|
||||||
|
.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 /*mods width*/ mw = plays.iter().map(|v| v.mods.to_string().len()).max().unwrap().max(4);
|
let pp = plays
|
||||||
let /*beatmap names*/ bw = beatmaps.iter().map(|v| v.len()).max().unwrap().max(7);
|
.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();
|
let mut m = MessageBuilder::new();
|
||||||
// Table header
|
// Table header
|
||||||
m.push_line(format!(" # | pp | accuracy | rank | {:mw$} | {:bw$}", "mods", "beatmap", mw = mw, bw = bw));
|
m.push_line(format!(
|
||||||
m.push_line(format!("---------------------------------{:-<mw$}---{:-<bw$}", "", "", mw = mw, bw = bw));
|
" # | {: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
|
// Each row
|
||||||
for (id, (play, beatmap)) in plays.iter().zip(beatmaps.iter()).enumerate() {
|
for (id, (play, beatmap)) in plays.iter().zip(beatmaps.iter()).enumerate() {
|
||||||
m.push_line(
|
m.push_line(format!(
|
||||||
format!(
|
"{:>3} | {:>pw$} | {:>8} | {:^4} | {:mw$} | {:bw$}",
|
||||||
"{:>3} | {:>6} | {:>8} | {:^4} | {:mw$} | {:bw$}",
|
|
||||||
id + start + 1,
|
id + start + 1,
|
||||||
play.pp.map(|v| format!("{:.2}", v)).unwrap_or("-".to_owned()),
|
pp[id],
|
||||||
format!("{:.2}%", play.accuracy(mode)),
|
format!("{:.2}%", play.accuracy(mode)),
|
||||||
play.rank.to_string(), play.mods.to_string(), beatmap, mw = mw, bw = bw));
|
play.rank.to_string(),
|
||||||
|
play.mods.to_string(),
|
||||||
|
beatmap,
|
||||||
|
pw = pw,
|
||||||
|
mw = mw,
|
||||||
|
bw = bw
|
||||||
|
));
|
||||||
}
|
}
|
||||||
// End
|
// End
|
||||||
let table = m.build().replace("```", "\\`\\`\\`");
|
let table = m.build().replace("```", "\\`\\`\\`");
|
||||||
let mut m = MessageBuilder::new();
|
let mut m = MessageBuilder::new();
|
||||||
m
|
m.push_codeblock(table, None).push_line(format!(
|
||||||
.push_codeblock(table, None)
|
"Page **{}/{}**",
|
||||||
.push_line(format!("Page **{}/{}**", page + 1, total_pages))
|
page + 1,
|
||||||
.push_line("Note: star difficulty don't reflect mods applied.");
|
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(()))
|
(e.content(m.build()), Ok(()))
|
||||||
}, std::time::Duration::from_secs(60))
|
},
|
||||||
|
std::time::Duration::from_secs(60),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[command]
|
#[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 user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
|
|
||||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
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
|
let user = osu
|
||||||
.user(user, |f| f.mode(mode))?
|
.user(user, |f| f.mode(mode))?
|
||||||
.ok_or(Error::from("User not found"))?;
|
.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()
|
.into_iter()
|
||||||
.last()
|
.last()
|
||||||
.ok_or(Error::from("No such play"))?;
|
.ok_or(Error::from("No such play"))?;
|
||||||
let beatmap = osu
|
let beatmap = meta_cache
|
||||||
.beatmaps(BeatmapRequestKind::Beatmap(recent_play.beatmap_id), |f| {
|
.get_beatmap(recent_play.beatmap_id, mode)
|
||||||
f.mode(mode, true)
|
|
||||||
})?
|
|
||||||
.into_iter()
|
|
||||||
.next()
|
|
||||||
.map(|v| BeatmapWithMode(v, mode))
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let content = oppai.get_beatmap(beatmap.beatmap_id)?;
|
||||||
|
let beatmap_mode = BeatmapWithMode(beatmap, mode);
|
||||||
|
|
||||||
msg.channel_id.send_message(&ctx, |m| {
|
msg.channel_id.send_message(&ctx, |m| {
|
||||||
m.content(format!(
|
m.content(format!(
|
||||||
"{}: here is the play that you requested",
|
"{}: here is the play that you requested",
|
||||||
msg.author
|
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...
|
// 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 => {
|
Nth::All => {
|
||||||
let plays = osu.user_recent(UserID::ID(user.id), |f| f.mode(mode).limit(50))?;
|
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]
|
#[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 => {
|
||||||
|
@ -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 user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
|
|
||||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
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
|
let user = osu
|
||||||
.user(user, |f| f)?
|
.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() {
|
for score in scores.into_iter() {
|
||||||
msg.channel_id.send_message(&ctx, |c| {
|
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 user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
|
|
||||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
|
let oppai = ctx.data.get_cloned::<BeatmapCache>();
|
||||||
let user = osu
|
let user = osu
|
||||||
.user(user, |f| f.mode(mode))?
|
.user(user, |f| f.mode(mode))?
|
||||||
.ok_or(Error::from("User not found"))?;
|
.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()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.map(|v| BeatmapWithMode(v, mode))
|
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
let content = oppai.get_beatmap(beatmap.beatmap_id)?;
|
||||||
|
let beatmap = BeatmapWithMode(beatmap, mode);
|
||||||
|
|
||||||
msg.channel_id.send_message(&ctx, |m| {
|
msg.channel_id.send_message(&ctx, |m| {
|
||||||
m.content(format!(
|
m.content(format!(
|
||||||
"{}: here is the play that you requested",
|
"{}: here is the play that you requested",
|
||||||
msg.author
|
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...
|
// 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 {
|
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 user = to_user_id_query(args.single::<UsernameArg>().ok(), &*ctx.data.read(), msg)?;
|
||||||
let osu = ctx.data.get_cloned::<OsuClient>();
|
let osu = ctx.data.get_cloned::<OsuClient>();
|
||||||
|
let cache = ctx.data.get_cloned::<BeatmapMetaCache>();
|
||||||
let user = osu.user(user, |f| f.mode(mode))?;
|
let user = osu.user(user, |f| f.mode(mode))?;
|
||||||
|
let oppai = ctx.data.get_cloned::<BeatmapCache>();
|
||||||
match user {
|
match user {
|
||||||
Some(u) => {
|
Some(u) => {
|
||||||
let best = osu
|
let best = osu
|
||||||
.user_best(UserID::ID(u.id), |f| f.limit(1).mode(mode))?
|
.user_best(UserID::ID(u.id), |f| f.limit(1).mode(mode))?
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.next()
|
.next()
|
||||||
.map(|m| {
|
.map(|m| -> Result<_, Error> {
|
||||||
osu.beatmaps(BeatmapRequestKind::Beatmap(m.beatmap_id), |f| {
|
let beatmap = cache.get_beatmap(m.beatmap_id, mode)?;
|
||||||
f.mode(mode, true)
|
let info = mode
|
||||||
|
.to_oppai_mode()
|
||||||
|
.map(|mode| -> Result<_, Error> {
|
||||||
|
Ok(oppai
|
||||||
|
.get_beatmap(m.beatmap_id)?
|
||||||
|
.get_info_with(Some(mode), m.mods)?)
|
||||||
})
|
})
|
||||||
.map(|map| (m, BeatmapWithMode(map.into_iter().next().unwrap(), mode)))
|
.transpose()?;
|
||||||
|
Ok((m, BeatmapWithMode(beatmap, mode), info))
|
||||||
})
|
})
|
||||||
.transpose()?;
|
.transpose()?;
|
||||||
msg.channel_id.send_message(&ctx, |m| {
|
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,
|
m.channel_id,
|
||||||
move |page: u8, e: &mut EditMessage| {
|
move |page: u8, e: &mut EditMessage| {
|
||||||
let start = (page as usize) * ITEMS_PER_PAGE;
|
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())));
|
return (e, Err(Error("No more items".to_owned())));
|
||||||
}
|
}
|
||||||
let total_len = users.len();
|
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();
|
let mut content = MessageBuilder::new();
|
||||||
content
|
content
|
||||||
.push_line("```")
|
.push_line("```")
|
||||||
.push_line("Rank | pp | Username")
|
.push_line("Rank | pp | Username")
|
||||||
.push_line("-------------------------");
|
.push_line(format!("-----------------{:-<uw$}", "", uw = username_len));
|
||||||
for (id, (pp, member)) in users.enumerate() {
|
for (id, (pp, member)) in users.iter().enumerate() {
|
||||||
content
|
content
|
||||||
.push(format!(
|
.push(format!(
|
||||||
"{:>4} | {:>7.2} | ",
|
"{:>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.
|
/// Client is the client that will perform calls to the osu! api server.
|
||||||
/// It's cheap to clone, so do it.
|
/// It's cheap to clone, so do it.
|
||||||
#[derive(Clone)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Client {
|
pub struct Client {
|
||||||
key: Arc<String>,
|
key: Arc<String>,
|
||||||
client: HTTPClient,
|
client: HTTPClient,
|
||||||
|
|
|
@ -45,6 +45,77 @@ pub struct Difficulty {
|
||||||
pub count_slider: u64,
|
pub count_slider: u64,
|
||||||
pub count_spinner: u64,
|
pub count_spinner: u64,
|
||||||
pub max_combo: Option<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)]
|
#[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 {
|
pub enum Mode {
|
||||||
Std,
|
Std,
|
||||||
Taiko,
|
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)]
|
#[derive(Clone, Debug, Serialize, Deserialize)]
|
||||||
pub struct Beatmap {
|
pub struct Beatmap {
|
||||||
// Beatmapset info
|
// Beatmapset info
|
||||||
|
@ -141,8 +244,6 @@ pub struct Beatmap {
|
||||||
pub beatmap_id: u64,
|
pub beatmap_id: u64,
|
||||||
pub difficulty_name: String,
|
pub difficulty_name: String,
|
||||||
pub difficulty: Difficulty,
|
pub difficulty: Difficulty,
|
||||||
pub drain_length: Duration,
|
|
||||||
pub total_length: Duration,
|
|
||||||
pub file_hash: String,
|
pub file_hash: String,
|
||||||
pub mode: Mode,
|
pub mode: Mode,
|
||||||
pub favourite_count: u64,
|
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.
|
/// Link to the cover image of the beatmap.
|
||||||
pub fn cover_url(&self) -> String {
|
pub fn cover_url(&self) -> String {
|
||||||
format!(
|
format!(
|
||||||
|
|
|
@ -128,3 +128,9 @@ impl fmt::Display for Mods {
|
||||||
Ok(())
|
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_slider: parse_from_str(&raw.count_slider)?,
|
||||||
count_spinner: parse_from_str(&raw.count_spinner)?,
|
count_spinner: parse_from_str(&raw.count_spinner)?,
|
||||||
max_combo: raw.max_combo.map(parse_from_str).transpose()?,
|
max_combo: raw.max_combo.map(parse_from_str).transpose()?,
|
||||||
},
|
|
||||||
drain_length: parse_duration(&raw.hit_length)?,
|
drain_length: parse_duration(&raw.hit_length)?,
|
||||||
total_length: parse_duration(&raw.total_length)?,
|
total_length: parse_duration(&raw.total_length)?,
|
||||||
|
},
|
||||||
file_hash: raw.file_md5,
|
file_hash: raw.file_md5,
|
||||||
mode: parse_mode(&raw.mode)?,
|
mode: parse_mode(&raw.mode)?,
|
||||||
favourite_count: parse_from_str(&raw.favourite_count)?,
|
favourite_count: parse_from_str(&raw.favourite_count)?,
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
pub(crate) struct Beatmap {
|
pub(crate) struct Beatmap {
|
||||||
pub approved: String,
|
pub approved: String,
|
||||||
pub submit_date: 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")]
|
#[command("register")]
|
||||||
#[description = "Register the current channel with an announcer"]
|
#[description = "Register the current channel with an announcer"]
|
||||||
#[usage = "[announcer key]"]
|
#[usage = "[announcer key]"]
|
||||||
|
@ -253,5 +298,5 @@ pub fn remove_announcer(ctx: &mut Context, m: &Message, mut args: Args) -> Comma
|
||||||
#[only_in(guilds)]
|
#[only_in(guilds)]
|
||||||
#[required_permissions(MANAGE_CHANNELS)]
|
#[required_permissions(MANAGE_CHANNELS)]
|
||||||
#[description = "Manage the announcers in the server."]
|
#[description = "Manage the announcers in the server."]
|
||||||
#[commands(remove_announcer, register_announcer)]
|
#[commands(remove_announcer, register_announcer, list_announcers)]
|
||||||
pub struct AnnouncerCommands;
|
pub struct AnnouncerCommands;
|
||||||
|
|
Loading…
Add table
Reference in a new issue