Implement score fetching

This commit is contained in:
Natsu Kagami 2024-02-13 23:13:20 +01:00
parent cf67a3e0e5
commit da39e6ed25
Signed by: nki
GPG key ID: 55A032EB38B49ADB
8 changed files with 192 additions and 58 deletions

31
Cargo.lock generated
View file

@ -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",

View file

@ -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"

View file

@ -90,14 +90,7 @@ impl Client {
) -> Result<Vec<Score>, Error> {
let mut r = ScoreRequestBuilder::new(beatmap_id);
f(&mut r);
let res: Vec<raw::Score> = r.build(self).await?.json().await?;
let mut res: Vec<Score> = 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(

View file

@ -558,6 +558,8 @@ pub struct Score {
pub count_geki: u64,
pub max_combo: u64,
pub perfect: bool,
pub lazer_build_id: Option<u32>,
}
impl Score {

View file

@ -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)),
}
}

View file

@ -69,6 +69,8 @@ impl TryFrom<raw::Score> 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,
})
}
}

View file

@ -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<Utc> {
}
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<rosu::recent_event::RecentEvent>,
) -> Self {
@ -129,8 +135,39 @@ impl From<rosu::recent_event::RecentEvent> for UserEvent {
}
}
impl From<rosu::score::Score> 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::<GameModsIntermode>()
.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<rosu::beatmap::Language> for Language {
}
}
}
impl From<rosu::Grade> 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<Mods> 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<rosu::mods::GameModsIntermode> 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)
}
}

View file

@ -110,6 +110,7 @@ fn handle_not_found<T>(v: Result<T, OsuError>) -> Result<Option<T>, 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<Response> {
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<Vec<models::Score>> {
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())
}
}