diff --git a/Cargo.lock b/Cargo.lock index cb73d97..8efe5f8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -899,20 +899,6 @@ dependencies = [ "want", ] -[[package]] -name = "hyper-rustls" -version = "0.23.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1788965e61b367cd03a62950836d5cd41560c3577d90e40e0819373194d1661c" -dependencies = [ - "http", - "hyper", - "rustls 0.20.9", - "rustls-native-certs", - "tokio", - "tokio-rustls 0.23.4", -] - [[package]] name = "hyper-rustls" version = "0.24.2" @@ -923,6 +909,7 @@ dependencies = [ "http", "hyper", "rustls 0.21.10", + "rustls-native-certs", "tokio", "tokio-rustls 0.24.1", ] @@ -1600,7 +1587,7 @@ dependencies = [ "http", "http-body", "hyper", - "hyper-rustls 0.24.2", + "hyper-rustls", "hyper-tls", "ipnet", "js-sys", @@ -1670,20 +1657,22 @@ checksum = "be9e281b71d3797817a1e6615dd8fb081dd61359b4c41d08792cc7c3c1c13b4e" [[package]] name = "rosu-v2" version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef969d8cb87f8dab58193bd722c7831fc839c9852c6e2aec64da3d2e87d447f3" +source = "git+https://github.com/natsukagami/rosu-v2?rev=6f6731cb2f0d235b006ab375dd94b446dde894ac#6f6731cb2f0d235b006ab375dd94b446dde894ac" dependencies = [ - "bitflags 1.3.2", "bytes", "dashmap", "futures", "hyper", - "hyper-rustls 0.23.2", + "hyper-rustls", + "itoa", "leaky-bucket-lite", "log", + "paste", "serde", "serde_json", + "serde_urlencoded", "smallstr", + "thiserror", "time", "tokio", "url", @@ -2010,9 +1999,9 @@ dependencies = [ [[package]] name = "smallstr" -version = "0.2.0" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e922794d168678729ffc7e07182721a14219c65814e66e91b839a272fe5ae4f" +checksum = "63b1aefdf380735ff8ded0b15f31aab05daf1f70216c01c02a12926badd1df9d" dependencies = [ "serde", "smallvec", diff --git a/youmubot-osu/Cargo.toml b/youmubot-osu/Cargo.toml index b400532..7ada79e 100644 --- a/youmubot-osu/Cargo.toml +++ b/youmubot-osu/Cargo.toml @@ -16,7 +16,7 @@ osuparse = { git = "https://github.com/eltrufas/osuparse", rev = "ad8f6e5e7771e7 regex = "1.5.6" reqwest = "0.11.10" rosu-pp = "0.9.1" -rosu-v2 = "0.8" +rosu-v2 = { git = "https://github.com/natsukagami/rosu-v2", rev = "6f6731cb2f0d235b006ab375dd94b446dde894ac" } time = "0.3" serde = { version = "1.0.137", features = ["derive"] } serenity = "0.11.2" diff --git a/youmubot-osu/src/lib.rs b/youmubot-osu/src/lib.rs index efda800..7c7fc94 100644 --- a/youmubot-osu/src/lib.rs +++ b/youmubot-osu/src/lib.rs @@ -90,14 +90,7 @@ impl Client { ) -> Result, Error> { let mut r = ScoreRequestBuilder::new(beatmap_id); f(&mut r); - let res: Vec = r.build(self).await?.json().await?; - let mut res: Vec = vec_try_into(res)?; - - // with a scores request you need to fill the beatmap ids yourself - res.iter_mut().for_each(|v| { - v.beatmap_id = beatmap_id; - }); - Ok(res) + r.build(self).await } pub async fn user_best( diff --git a/youmubot-osu/src/models/mod.rs b/youmubot-osu/src/models/mod.rs index 1946ed4..1d866e0 100644 --- a/youmubot-osu/src/models/mod.rs +++ b/youmubot-osu/src/models/mod.rs @@ -558,6 +558,8 @@ pub struct Score { pub count_geki: u64, pub max_combo: u64, pub perfect: bool, + + pub lazer_build_id: Option, } impl Score { diff --git a/youmubot-osu/src/models/mods.rs b/youmubot-osu/src/models/mods.rs index aef87f1..ffa01bb 100644 --- a/youmubot-osu/src/models/mods.rs +++ b/youmubot-osu/src/models/mods.rs @@ -40,6 +40,10 @@ bitflags::bitflags! { const NOVIDEO = Self::TD.bits; /* never forget */ const SPEED_CHANGING = Self::DT.bits | Self::HT.bits | Self::NC.bits; const MAP_CHANGING = Self::HR.bits | Self::EZ.bits | Self::SPEED_CHANGING.bits; + + // Made up flags + const CLASSIC = 1 << 59; + const UNKNOWN = 1 << 60; } } @@ -68,6 +72,8 @@ const MODS_WITH_NAMES: &[(Mods, &str)] = &[ (Mods::KEY7, "7K"), (Mods::KEY8, "8K"), (Mods::KEY9, "9K"), + (Mods::CLASSIC, "CL"), + (Mods::UNKNOWN, "??"), ]; impl std::str::FromStr for Mods { @@ -106,6 +112,8 @@ impl std::str::FromStr for Mods { "7K" => res |= Mods::KEY7, "8K" => res |= Mods::KEY8, "9K" => res |= Mods::KEY9, + "CL" => res |= Mods::CLASSIC, + "??" => res |= Mods::UNKNOWN, v => return Err(format!("{} is not a valid mod", v)), } } diff --git a/youmubot-osu/src/models/parse.rs b/youmubot-osu/src/models/parse.rs index 5ae9760..a35664a 100644 --- a/youmubot-osu/src/models/parse.rs +++ b/youmubot-osu/src/models/parse.rs @@ -69,6 +69,8 @@ impl TryFrom for Score { count_geki: parse_from_str(&raw.countgeki)?, max_combo: parse_from_str(&raw.maxcombo)?, perfect: parse_bool(&raw.perfect)?, + + lazer_build_id: None, }) } } diff --git a/youmubot-osu/src/models/rosu.rs b/youmubot-osu/src/models/rosu.rs index c7d9714..c42dc73 100644 --- a/youmubot-osu/src/models/rosu.rs +++ b/youmubot-osu/src/models/rosu.rs @@ -1,4 +1,7 @@ -use rosu_v2::model as rosu; +use rosu_v2::model::{ + self as rosu, + mods::{GameModIntermode, GameModsIntermode}, +}; use super::*; @@ -25,7 +28,10 @@ fn time_to_utc(s: time::OffsetDateTime) -> DateTime { } impl Beatmap { - pub(crate) fn from_rosu(bm: rosu::beatmap::Beatmap, set: &rosu::beatmap::Beatmapset) -> Self { + pub(crate) fn from_rosu( + bm: rosu::beatmap::BeatmapExtended, + set: &rosu::beatmap::BeatmapsetExtended, + ) -> Self { let last_updated = time_to_utc(bm.last_updated); let difficulty = Difficulty::from_rosu(&bm); Self { @@ -68,7 +74,7 @@ impl Beatmap { impl User { pub(crate) fn from_rosu( - user: rosu::user::User, + user: rosu::user::UserExtended, stats: rosu::user::UserStatistics, events: Vec, ) -> Self { @@ -129,8 +135,39 @@ impl From for UserEvent { } } +impl From for Score { + fn from(s: rosu::score::Score) -> Self { + let legacy_stats = s.statistics.as_legacy(s.mode); + Self { + id: Some(s.id), + user_id: s.user_id as u64, + date: time_to_utc(s.ended_at), + replay_available: s.replay, + beatmap_id: s.map_id as u64, + score: s.score as u64, + pp: s.pp.map(|v| v as f64), + rank: s.grade.into(), + mods: s + .mods + .iter() + .map(|v| v.intermode()) + .collect::() + .into(), + count_300: legacy_stats.count_300 as u64, + count_100: legacy_stats.count_100 as u64, + count_50: legacy_stats.count_50 as u64, + count_miss: legacy_stats.count_miss as u64, + count_katu: legacy_stats.count_katu as u64, + count_geki: legacy_stats.count_geki as u64, + max_combo: s.max_combo as u64, + perfect: s.is_perfect_combo, + lazer_build_id: s.build_id, + } + } +} + impl Difficulty { - pub(crate) fn from_rosu(bm: &rosu::beatmap::Beatmap) -> Self { + pub(crate) fn from_rosu(bm: &rosu::beatmap::BeatmapExtended) -> Self { Self { stars: bm.stars as f64, aim: None, @@ -214,3 +251,95 @@ impl From for Language { } } } + +impl From for Rank { + fn from(value: rosu::Grade) -> Self { + match value { + rosu::Grade::F => Rank::F, + rosu::Grade::D => Rank::D, + rosu::Grade::C => Rank::C, + rosu::Grade::B => Rank::B, + rosu::Grade::A => Rank::A, + rosu::Grade::S => Rank::S, + rosu::Grade::SH => Rank::SH, + rosu::Grade::X => Rank::SS, + rosu::Grade::XH => Rank::SSH, + } + } +} + +impl From for rosu::mods::GameModsIntermode { + fn from(value: Mods) -> Self { + let mut res = GameModsIntermode::new(); + const MOD_MAP: &[(Mods, GameModIntermode)] = &[ + (Mods::NF, GameModIntermode::NoFail), + (Mods::EZ, GameModIntermode::Easy), + (Mods::TD, GameModIntermode::TouchDevice), + (Mods::HD, GameModIntermode::Hidden), + (Mods::HR, GameModIntermode::HardRock), + (Mods::SD, GameModIntermode::SuddenDeath), + (Mods::DT, GameModIntermode::DoubleTime), + (Mods::RX, GameModIntermode::Relax), + (Mods::HT, GameModIntermode::HalfTime), + (Mods::NC, GameModIntermode::Nightcore), + (Mods::FL, GameModIntermode::Flashlight), + (Mods::AT, GameModIntermode::Autoplay), + (Mods::SO, GameModIntermode::SpunOut), + (Mods::AP, GameModIntermode::Autopilot), + (Mods::PF, GameModIntermode::Perfect), + (Mods::KEY1, GameModIntermode::OneKey), + (Mods::KEY2, GameModIntermode::TwoKeys), + (Mods::KEY3, GameModIntermode::ThreeKeys), + (Mods::KEY4, GameModIntermode::FourKeys), + (Mods::KEY5, GameModIntermode::FiveKeys), + (Mods::KEY6, GameModIntermode::SixKeys), + (Mods::KEY7, GameModIntermode::SevenKeys), + (Mods::KEY8, GameModIntermode::EightKeys), + (Mods::KEY9, GameModIntermode::NineKeys), + ]; + for (m1, m2) in MOD_MAP { + if value.contains(*m1) { + res.insert(*m2); + } + } + res + } +} + +impl From for Mods { + fn from(value: rosu_v2::prelude::GameModsIntermode) -> Self { + value + .into_iter() + .map(|m| match m { + GameModIntermode::NoFail => Mods::NF, + GameModIntermode::Easy => Mods::EZ, + GameModIntermode::TouchDevice => Mods::TD, + GameModIntermode::Hidden => Mods::HD, + GameModIntermode::HardRock => Mods::HR, + GameModIntermode::SuddenDeath => Mods::SD, + GameModIntermode::DoubleTime => Mods::DT, + GameModIntermode::Relax => Mods::RX, + GameModIntermode::HalfTime => Mods::HT, + GameModIntermode::Nightcore => Mods::NC, + GameModIntermode::Flashlight => Mods::FL, + GameModIntermode::Autoplay => Mods::AT, + GameModIntermode::SpunOut => Mods::SO, + GameModIntermode::Autopilot => Mods::AP, + GameModIntermode::Perfect => Mods::PF, + GameModIntermode::OneKey => Mods::KEY1, + GameModIntermode::TwoKeys => Mods::KEY2, + GameModIntermode::ThreeKeys => Mods::KEY3, + GameModIntermode::FourKeys => Mods::KEY4, + GameModIntermode::FiveKeys => Mods::KEY5, + GameModIntermode::SixKeys => Mods::KEY6, + GameModIntermode::SevenKeys => Mods::KEY7, + GameModIntermode::EightKeys => Mods::KEY8, + GameModIntermode::NineKeys => Mods::KEY9, + GameModIntermode::Classic => Mods::CLASSIC, + _ => Mods::UNKNOWN, + }) + .fold(Mods::NOMOD, |a, b| a | b) + + // Mods::from_bits_truncate(value.bits() as u64) + } +} diff --git a/youmubot-osu/src/request.rs b/youmubot-osu/src/request.rs index f8da172..0ac2f87 100644 --- a/youmubot-osu/src/request.rs +++ b/youmubot-osu/src/request.rs @@ -110,6 +110,7 @@ fn handle_not_found(v: Result) -> Result, OsuError> { pub mod builders { use reqwest::Response; + use rosu_v2::model::mods::GameModsIntermode; use crate::models; @@ -213,19 +214,6 @@ pub mod builders { events.retain(|e| (now <= e.created_at)); let stats = user.statistics.take().unwrap(); Ok(Some(models::User::from_rosu(user, stats, events))) - // Ok(client - // .build_request("https://osu.ppy.sh/api/get_user") - // .await? - // .query(&self.user.to_query()) - // .query(&self.mode.to_query()) - // .query( - // &self - // .event_days - // .map(|v| ("event_days", v.to_string())) - // .to_query(), - // ) - // .send() - // .await?) } } @@ -268,17 +256,40 @@ pub mod builders { self } - pub(crate) async fn build(&self, client: &Client) -> Result { - Ok(client - .build_request("https://osu.ppy.sh/api/get_scores") - .await? - .query(&[("b", self.beatmap_id)]) - .query(&self.user.to_query()) - .query(&self.mode.to_query()) - .query(&self.mods.to_query()) - .query(&self.limit.map(|v| ("limit", v.to_string())).to_query()) - .send() - .await?) + pub(crate) async fn build(self, client: &Client) -> Result> { + let scores = handle_not_found(match self.user { + Some(user) => { + let mut r = client + .rosu + .beatmap_user_scores(self.beatmap_id as u32, user); + if let Some(mode) = self.mode { + r = r.mode(mode.into()); + } + match self.mods { + Some(mods) => r.await.map(|mut ss| { + let mods = GameModsIntermode::from(mods); + ss.retain(|s| mods.iter().all(|m| s.mods.contains_intermode(m))); + ss + }), + None => r.await, + } + } + None => { + let mut r = client.rosu.beatmap_scores(self.beatmap_id as u32).global(); + if let Some(mode) = self.mode { + r = r.mode(mode.into()); + } + if let Some(mods) = self.mods { + r = r.mods(GameModsIntermode::from(mods)); + } + if let Some(limit) = self.limit { + r = r.limit(limit as u32); + } + r.await + } + })? + .unwrap_or(vec![]); + Ok(scores.into_iter().map(|v| v.into()).collect()) } }