Drop old homegrown Mods for rosu_v2::Mods, implement rate change support (#52)

* Move mods to intermode wrapper

* Update rust to 1.79

* Move mods from homebrewed impl to rosu

* Display mod details

* Take clock-rate into account when calculating pp

* Allow specifying rate in mods input

* Formatting

* Fix clippy
This commit is contained in:
Natsu Kagami 2024-08-24 21:21:01 +00:00 committed by GitHub
parent 7565a6e5c5
commit 735b382102
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 788 additions and 443 deletions

35
flake.lock generated
View file

@ -7,11 +7,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1718730147, "lastModified": 1724377159,
"narHash": "sha256-QmD6B6FYpuoCqu6ZuPJH896ItNquDkn0ulQlOn4ykN8=", "narHash": "sha256-ixjje1JO8ucKT41hs6n2NCde1Vc0+Zc2p2gUbJpCsMw=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "32c21c29b034d0a93fdb2379d6fabc40fc3d0e6c", "rev": "3e47b7a86c19142bd3675da49d6acef488b4dac1",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -40,11 +40,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1718530797, "lastModified": 1724224976,
"narHash": "sha256-pup6cYwtgvzDpvpSCFh1TEUjw2zkNpk8iolbKnyFmmU=", "narHash": "sha256-Z/ELQhrSd7bMzTO8r7NZgi9g5emh+aRKoCdaAv5fiO0=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b60ebf54c15553b393d144357375ea956f89e9a9", "rev": "c374d94f1536013ca8e92341b540eba4c22f9c62",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -58,7 +58,28 @@
"inputs": { "inputs": {
"crane": "crane", "crane": "crane",
"flake-utils": "flake-utils", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1724466314,
"narHash": "sha256-ltKuK6shQ64uej1mYNtBsDYxttUNFiv9AcHqk0+0NQM=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "2b5b3edd96ef336b00622dcabc13788fdef9e3ca",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
} }
}, },
"systems": { "systems": {

View file

@ -7,16 +7,24 @@
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
}; };
nixConfig = { nixConfig = {
extra-substituters = [ "https://natsukagami.cachix.org" ]; extra-substituters = [ "https://natsukagami.cachix.org" ];
trusted-public-keys = [ "natsukagami.cachix.org-1:3U6GV8i8gWEaXRUuXd2S4ASfYgdl2QFPWg4BKPbmYiQ=" ]; trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" "natsukagami.cachix.org-1:3U6GV8i8gWEaXRUuXd2S4ASfYgdl2QFPWg4BKPbmYiQ=" ];
}; };
outputs = { self, nixpkgs, flake-utils, ... }@inputs: flake-utils.lib.eachDefaultSystem outputs = { self, nixpkgs, flake-utils, ... }@inputs: flake-utils.lib.eachDefaultSystem
(system: (system:
let let
pkgs = import nixpkgs { inherit system; }; pkgs = import nixpkgs
craneLib = inputs.crane.mkLib pkgs; {
inherit system; overlays = [ (import inputs.rust-overlay) ];
};
craneLib = (inputs.crane.mkLib pkgs).overrideToolchain (p: p.rust-bin.stable."1.79.0".default);
# craneLib = inputs.crane.mkLib pkgs;
in in
rec { rec {
packages.youmubot = pkgs.callPackage ./package.nix { inherit craneLib; }; packages.youmubot = pkgs.callPackage ./package.nix { inherit craneLib; };
@ -35,7 +43,7 @@
{ {
inputsFrom = [ packages.youmubot ]; inputsFrom = [ packages.youmubot ];
buildInputs = with pkgs; [ rustc rustfmt clippy sqlx-cli rust-analyzer ]; buildInputs = with pkgs; [ rustfmt clippy sqlx-cli rust-analyzer ];
nativeBuildInputs = nixpkgs.lib.optionals pkgs.stdenv.isLinux (with pkgs; [ nativeBuildInputs = nixpkgs.lib.optionals pkgs.stdenv.isLinux (with pkgs; [
pkg-config pkg-config

1
rust-toolchain Normal file
View file

@ -0,0 +1 @@
1.79.0

View file

@ -27,7 +27,7 @@ use crate::{
discord::oppai_cache::BeatmapContent, discord::oppai_cache::BeatmapContent,
models::{Mode, Score, User, UserEventRank}, models::{Mode, Score, User, UserEventRank},
request::UserID, request::UserID,
Client as Osu, OsuClient as Osu,
}; };
use super::db::OsuUser; use super::db::OsuUser;

View file

@ -5,14 +5,14 @@ use youmubot_prelude::*;
use crate::{ use crate::{
models::{ApprovalStatus, Beatmap, Mode}, models::{ApprovalStatus, Beatmap, Mode},
Client, OsuClient,
}; };
/// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling. /// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling.
/// Does not cache non-Ranked beatmaps. /// Does not cache non-Ranked beatmaps.
#[derive(Clone)] #[derive(Clone)]
pub struct BeatmapMetaCache { pub struct BeatmapMetaCache {
client: Arc<Client>, client: Arc<OsuClient>,
pool: Pool, pool: Pool,
} }
@ -28,7 +28,7 @@ impl TypeMapKey for BeatmapMetaCache {
impl BeatmapMetaCache { impl BeatmapMetaCache {
/// Create a new beatmap cache. /// Create a new beatmap cache.
pub fn new(client: Arc<Client>, pool: Pool) -> Self { pub fn new(client: Arc<OsuClient>, pool: Pool) -> Self {
BeatmapMetaCache { client, pool } BeatmapMetaCache { client, pool }
} }

View file

@ -211,7 +211,7 @@ mod scores {
let beatmap = meta_cache.get_beatmap(play.beatmap_id, mode).await?; let beatmap = meta_cache.get_beatmap(play.beatmap_id, mode).await?;
let info = { let info = {
let b = oppai.get_beatmap(beatmap.beatmap_id).await?; let b = oppai.get_beatmap(beatmap.beatmap_id).await?;
b.get_info_with(mode, play.mods).ok() b.get_info_with(mode, &play.mods).ok()
}; };
Ok((beatmap, info)) as Result<(Beatmap, Option<BeatmapInfo>)> Ok((beatmap, info)) as Result<(Beatmap, Option<BeatmapInfo>)>
}) })
@ -236,7 +236,7 @@ mod scores {
p.count_50, p.count_50,
p.count_miss, p.count_miss,
), ),
p.mods, &p.mods,
) )
.ok() .ok()
.map(|pp| format!("{:.2}[?]", pp)) .map(|pp| format!("{:.2}[?]", pp))
@ -291,7 +291,7 @@ mod scores {
beatmap.artist, beatmap.artist,
beatmap.title, beatmap.title,
beatmap.difficulty_name, beatmap.difficulty_name,
beatmap.short_link(Some(self.mode), Some(play.mods)), beatmap.short_link(Some(self.mode), &play.mods),
) )
}) })
.unwrap_or_else(|| "FETCH_FAILED".to_owned()) .unwrap_or_else(|| "FETCH_FAILED".to_owned())
@ -359,13 +359,11 @@ mod beatmapset {
ctx: &Context, ctx: &Context,
beatmapset: Vec<Beatmap>, beatmapset: Vec<Beatmap>,
mode: Option<Mode>, mode: Option<Mode>,
mods: Option<Mods>, mods: Mods,
reply_to: &Message, reply_to: &Message,
guild_id: Option<GuildId>, guild_id: Option<GuildId>,
message: impl AsRef<str>, message: impl AsRef<str>,
) -> Result<bool> { ) -> Result<bool> {
let mods = mods.unwrap_or(Mods::NOMOD);
if beatmapset.is_empty() { if beatmapset.is_empty() {
return Ok(false); return Ok(false);
} }
@ -405,7 +403,7 @@ mod beatmapset {
env.oppai env.oppai
.get_beatmap(b.beatmap_id) .get_beatmap(b.beatmap_id)
.await .await
.and_then(move |v| v.get_possible_pp_with(self.mode.unwrap_or(b.mode), self.mods)) .and_then(move |v| v.get_possible_pp_with(self.mode.unwrap_or(b.mode), &self.mods))
} }
} }
@ -446,7 +444,7 @@ mod beatmapset {
crate::discord::embeds::beatmap_embed( crate::discord::embeds::beatmap_embed(
map, map,
self.mode.unwrap_or(map.mode), self.mode.unwrap_or(map.mode),
self.mods, &self.mods,
info, info,
) )
.footer({ .footer({

View file

@ -25,7 +25,7 @@ pub(crate) fn grouped_number(num: u64) -> String {
b.build() b.build()
} }
fn beatmap_description(b: &Beatmap) -> String { fn beatmap_description(b: &Beatmap, mods: &Mods) -> String {
MessageBuilder::new() MessageBuilder::new()
.push_bold_line(b.approval.to_string()) .push_bold_line(b.approval.to_string())
.push({ .push({
@ -59,13 +59,24 @@ fn beatmap_description(b: &Beatmap) -> String {
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join(" "), .join(" "),
) )
.push_line(mod_details(mods).unwrap_or("".into()))
.build() .build()
} }
fn mod_details(mods: &Mods) -> Option<Cow<'_, str>> {
let mut d = mods.details();
if d.is_empty() {
None
} else {
d.insert(0, "**Mods**:".to_owned());
Some(Cow::from(d.join("\n- ")))
}
}
pub fn beatmap_offline_embed( pub fn beatmap_offline_embed(
b: &'_ crate::discord::oppai_cache::BeatmapContent, b: &'_ crate::discord::oppai_cache::BeatmapContent,
m: Mode, m: Mode,
mods: Mods, mods: &Mods,
) -> Result<(CreateEmbed, Vec<CreateAttachment>)> { ) -> Result<(CreateEmbed, Vec<CreateAttachment>)> {
let bm = b.content.clone(); let bm = b.content.clone();
let metadata = b.metadata.clone(); let metadata = b.metadata.clone();
@ -150,7 +161,7 @@ fn beatmap_title(
artist: impl AsRef<str>, artist: impl AsRef<str>,
title: impl AsRef<str>, title: impl AsRef<str>,
difficulty: impl AsRef<str>, difficulty: impl AsRef<str>,
mods: Mods, mods: &Mods,
) -> String { ) -> String {
let mod_str = if mods == Mods::NOMOD { let mod_str = if mods == Mods::NOMOD {
"".to_owned() "".to_owned()
@ -168,7 +179,7 @@ fn beatmap_title(
.build() .build()
} }
pub fn beatmap_embed(b: &'_ Beatmap, m: Mode, mods: Mods, info: BeatmapInfoWithPP) -> CreateEmbed { pub fn beatmap_embed(b: &'_ Beatmap, m: Mode, mods: &Mods, info: BeatmapInfoWithPP) -> CreateEmbed {
let diff = b.difficulty.apply_mods(mods, info.0.stars); let diff = b.difficulty.apply_mods(mods, info.0.stars);
CreateEmbed::new() CreateEmbed::new()
.title(beatmap_title(&b.artist, &b.title, &b.difficulty_name, mods)) .title(beatmap_title(&b.artist, &b.title, &b.difficulty_name, mods))
@ -192,7 +203,7 @@ pub fn beatmap_embed(b: &'_ Beatmap, m: Mode, mods: Mods, info: BeatmapInfoWithP
)) ))
}) })
.field("Information", diff.format_info(m, mods, b), false) .field("Information", diff.format_info(m, mods, b), false)
.description(beatmap_description(b)) .description(beatmap_description(b, mods))
} }
const MAX_DIFFS: usize = 25 - 4; const MAX_DIFFS: usize = 25 - 4;
@ -222,7 +233,7 @@ pub fn beatmapset_embed(bs: &'_ [Beatmap], m: Option<Mode>) -> CreateEmbed {
b.beatmapset_id b.beatmapset_id
)) ))
.color(0xffb6c1) .color(0xffb6c1)
.description(beatmap_description(b)) .description(beatmap_description(b, Mods::NOMOD))
.fields(bs.iter().rev().take(MAX_DIFFS).rev().map(|b: &Beatmap| { .fields(bs.iter().rev().take(MAX_DIFFS).rev().map(|b: &Beatmap| {
( (
format!("[{}]", b.difficulty_name), format!("[{}]", b.difficulty_name),
@ -292,7 +303,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
let content = self.content; let content = self.content;
let u = &self.u; let u = &self.u;
let accuracy = s.accuracy(mode); let accuracy = s.accuracy(mode);
let info = content.get_info_with(mode, s.mods).ok(); let info = content.get_info_with(mode, &s.mods).ok();
let stars = info let stars = info
.as_ref() .as_ref()
.map(|info| info.stars) .map(|info| info.stars)
@ -322,7 +333,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
mode, mode,
Some(s.max_combo as usize), Some(s.max_combo as usize),
Accuracy::ByCount(s.count_300, s.count_100, s.count_50, s.count_miss), Accuracy::ByCount(s.count_300, s.count_100, s.count_50, s.count_miss),
s.mods, &s.mods,
) )
.ok() .ok()
.map(|pp| (pp, format!("{:.2}pp [?]", pp))) .map(|pp| (pp, format!("{:.2}pp [?]", pp)))
@ -333,7 +344,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
mode, mode,
None, None,
Accuracy::ByCount(s.count_300 + s.count_miss, s.count_100, s.count_50, 0), Accuracy::ByCount(s.count_300 + s.count_miss, s.count_100, s.count_50, 0),
s.mods, &s.mods,
) )
.ok() .ok()
.filter(|&v| pp.as_ref().map(|&(origin, _)| origin < v).unwrap_or(false)) .filter(|&v| pp.as_ref().map(|&(origin, _)| origin < v).unwrap_or(false))
@ -377,12 +388,34 @@ impl<'a> ScoreEmbedBuilder<'a> {
.or(s.global_rank) .or(s.global_rank)
.map(|v| format!(" | #{} on Global Rankings!", v)) .map(|v| format!(" | #{} on Global Rankings!", v))
.unwrap_or_else(|| "".to_owned()); .unwrap_or_else(|| "".to_owned());
let diff = b.difficulty.apply_mods(s.mods, stars); let diff = b.difficulty.apply_mods(&s.mods, stars);
let creator = if b.difficulty_name.contains("'s") { let creator = if b.difficulty_name.contains("'s") {
"".to_owned() "".to_owned()
} else { } else {
format!("by {} ", b.creator) format!("by {} ", b.creator)
}; };
let mod_details = mod_details(&s.mods);
let description_fields = [
Some(
format!(
"**Played**: {} {} {}",
s.date.format("<t:%s:R>"),
s.link()
.map(|s| format!("[[Score]]({})", s).into())
.unwrap_or(Cow::from("")),
s.replay_download_link()
.map(|s| format!("[[Replay]]({})", s).into())
.unwrap_or(Cow::from("")),
)
.into(),
),
pp_gained.as_ref().map(|v| (&v[..]).into()),
mod_details,
]
.into_iter()
.flatten()
.collect::<Vec<_>>()
.join("\n");
let mut m = CreateEmbed::new() let mut m = CreateEmbed::new()
.author( .author(
CreateEmbedAuthor::new(&u.username) CreateEmbedAuthor::new(&u.username)
@ -411,18 +444,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
.push(world_record) .push(world_record)
.build(), .build(),
) )
.description(format!( .description(description_fields)
r#"**Played**: {} {} {}
{}"#,
s.date.format("<t:%s:R>"),
s.link()
.map(|s| format!("[[Score]]({})", s).into())
.unwrap_or(Cow::from("")),
s.replay_download_link()
.map(|s| format!("[[Replay]]({})", s).into())
.unwrap_or(Cow::from("")),
pp_gained.as_ref().map(|v| &v[..]).unwrap_or(""),
))
.thumbnail(b.thumbnail_url()) .thumbnail(b.thumbnail_url())
.field( .field(
"Score stats", "Score stats",
@ -442,9 +464,9 @@ impl<'a> ScoreEmbedBuilder<'a> {
), ),
true, true,
) )
.field("Map stats", diff.format_info(mode, s.mods, b), false); .field("Map stats", diff.format_info(mode, &s.mods, b), false);
let mut footer = self.footer.take().unwrap_or_default(); let mut footer = self.footer.take().unwrap_or_default();
if mode != Mode::Std && s.mods != Mods::NOMOD { if mode != Mode::Std && &s.mods != Mods::NOMOD {
footer += " Star difficulty does not reflect game mods."; footer += " Star difficulty does not reflect game mods.";
} }
if !footer.is_empty() { if !footer.is_empty() {
@ -557,8 +579,8 @@ pub(crate) fn user_embed(
.push(format!( .push(format!(
"> {}", "> {}",
map.difficulty map.difficulty
.apply_mods(v.mods, info.stars) .apply_mods(&v.mods, info.stars)
.format_info(mode, v.mods, &map) .format_info(mode, &v.mods, &map)
.replace('\n', "\n> ") .replace('\n', "\n> ")
)) ))
.build(), .build(),

View file

@ -123,10 +123,11 @@ pub fn dot_osu_hook<'a>(
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone(); let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
let (beatmap, _) = env.oppai.download_beatmap_from_url(&url).await.ok()?; let (beatmap, _) = env.oppai.download_beatmap_from_url(&url).await.ok()?;
let m = Mode::from(beatmap.content.mode as u8);
crate::discord::embeds::beatmap_offline_embed( crate::discord::embeds::beatmap_offline_embed(
&beatmap, &beatmap,
Mode::from(beatmap.content.mode as u8), /*For now*/ m, /*For now*/
msg.content.trim().parse().unwrap_or(Mods::NOMOD), &Mods::from_str(msg.content.trim(), m).unwrap_or_default(),
) )
.pls_ok() .pls_ok()
} }
@ -152,10 +153,11 @@ pub fn dot_osu_hook<'a>(
beatmaps beatmaps
.into_iter() .into_iter()
.filter_map(|beatmap| { .filter_map(|beatmap| {
let m = Mode::from(beatmap.content.mode as u8);
crate::discord::embeds::beatmap_offline_embed( crate::discord::embeds::beatmap_offline_embed(
&beatmap, &beatmap,
Mode::from(beatmap.content.mode as u8), /*For now*/ m, /*For now*/
msg.content.trim().parse().unwrap_or(Mods::NOMOD), &Mods::from_str(msg.content.trim(), m).unwrap_or_default(),
) )
.pls_ok() .pls_ok()
}) })
@ -300,7 +302,7 @@ async fn handle_beatmap<'a, 'b>(
.embed(beatmap_embed( .embed(beatmap_embed(
beatmap, beatmap,
mode.unwrap_or(beatmap.mode), mode.unwrap_or(beatmap.mode),
mods, &mods,
info, info,
)) ))
.components(vec![beatmap_components(reply_to.guild_id)]) .components(vec![beatmap_components(reply_to.guild_id)])
@ -321,7 +323,7 @@ async fn handle_beatmapset<'a, 'b>(
ctx, ctx,
beatmaps, beatmaps,
mode, mode,
None, Mods::default(),
reply_to, reply_to,
reply_to.guild_id, reply_to.guild_id,
format!("Beatmapset information for `{}`", link), format!("Beatmapset information for `{}`", link),

View file

@ -147,18 +147,21 @@ pub fn handle_last_button<'a>(
.unwrap(); .unwrap();
let BeatmapWithMode(b, m) = &bm; let BeatmapWithMode(b, m) = &bm;
let mods = mods_def.unwrap_or(Mods::NOMOD); let mods = mods_def.unwrap_or_default();
let info = env let info = env
.oppai .oppai
.get_beatmap(b.beatmap_id) .get_beatmap(b.beatmap_id)
.await? .await?
.get_possible_pp_with(*m, mods)?; .get_possible_pp_with(*m, &mods)?;
comp.create_response( comp.create_response(
&ctx, &ctx,
serenity::all::CreateInteractionResponse::Message( serenity::all::CreateInteractionResponse::Message(
CreateInteractionResponseMessage::new() CreateInteractionResponseMessage::new()
.content(format!("Information for beatmap `{}`", bm.short_link(mods))) .content(format!(
.embed(beatmap_embed(b, *m, mods, info)) "Information for beatmap `{}`",
bm.short_link(&mods)
))
.embed(beatmap_embed(b, *m, &mods, info))
.components(vec![beatmap_components(comp.guild_id)]), .components(vec![beatmap_components(comp.guild_id)]),
), ),
) )

View file

@ -2,6 +2,7 @@ use std::str::FromStr;
use crate::models::*; use crate::models::*;
use lazy_static::lazy_static; use lazy_static::lazy_static;
use mods::UnparsedMods;
use regex::Regex; use regex::Regex;
use stream::Stream; use stream::Stream;
use youmubot_prelude::*; use youmubot_prelude::*;
@ -22,7 +23,7 @@ pub struct ToPrint<'a> {
lazy_static! { lazy_static! {
// Beatmap(set) hooks // Beatmap(set) hooks
static ref OLD_LINK_REGEX: Regex = Regex::new( static ref OLD_LINK_REGEX: Regex = Regex::new(
r"(?:https?://)?osu\.ppy\.sh/(?P<link_type>s|b)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>[0123]))?(?:\+(?P<mods>[A-Z]+))?" r"(?:https?://)?osu\.ppy\.sh/(?P<link_type>s|b|beatmaps)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>[0123]))?(?:\+(?P<mods>[A-Z]+))?"
).unwrap(); ).unwrap();
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]+))?"
@ -51,12 +52,12 @@ pub fn parse_old_links<'a>(
.transpose()? .transpose()?
.map(Mode::from); .map(Mode::from);
let embed = match req_type { let embed = match req_type {
"b" => { "b" | "beatmaps" => {
// collect beatmap info // collect beatmap info
let mods = capture let mods = capture
.name("mods") .name("mods")
.and_then(|v| Mods::from_str(v.as_str()).pls_ok()) .and_then(|v| UnparsedMods::from_str(v.as_str()).pls_ok())
.unwrap_or(Mods::NOMOD); .unwrap_or_default();
EmbedType::from_beatmap_id(env, capture["id"].parse()?, mode, mods).await EmbedType::from_beatmap_id(env, capture["id"].parse()?, mode, mods).await
} }
"s" => EmbedType::from_beatmapset_id(env, capture["id"].parse()?).await, "s" => EmbedType::from_beatmapset_id(env, capture["id"].parse()?).await,
@ -90,8 +91,8 @@ pub fn parse_new_links<'a>(
Some(beatmap_id) => { Some(beatmap_id) => {
let mods = capture let mods = capture
.name("mods") .name("mods")
.and_then(|v| Mods::from_str(v.as_str()).pls_ok()) .and_then(|v| UnparsedMods::from_str(v.as_str()).pls_ok())
.unwrap_or(Mods::NOMOD); .unwrap_or_default();
EmbedType::from_beatmap_id(env, beatmap_id, mode, mods).await EmbedType::from_beatmap_id(env, beatmap_id, mode, mods).await
} }
None => { None => {
@ -122,8 +123,8 @@ pub fn parse_short_links<'a>(
let id: u64 = capture.name("id").unwrap().as_str().parse()?; let id: u64 = capture.name("id").unwrap().as_str().parse()?;
let mods = capture let mods = capture
.name("mods") .name("mods")
.and_then(|v| Mods::from_str(v.as_str()).pls_ok()) .and_then(|v| UnparsedMods::from_str(v.as_str()).pls_ok())
.unwrap_or(Mods::NOMOD); .unwrap_or_default();
let embed = EmbedType::from_beatmap_id(env, id, mode, mods).await?; let embed = EmbedType::from_beatmap_id(env, id, mode, mods).await?;
Ok(ToPrint { embed, link, mode }) Ok(ToPrint { embed, link, mode })
}) })
@ -136,18 +137,19 @@ impl EmbedType {
env: &OsuEnv, env: &OsuEnv,
beatmap_id: u64, beatmap_id: u64,
mode: Option<Mode>, mode: Option<Mode>,
mods: Mods, mods: UnparsedMods,
) -> Result<Self> { ) -> Result<Self> {
let bm = match mode { let bm = match mode {
Some(mode) => env.beatmaps.get_beatmap(beatmap_id, mode).await?, Some(mode) => env.beatmaps.get_beatmap(beatmap_id, mode).await?,
None => env.beatmaps.get_beatmap_default(beatmap_id).await?, None => env.beatmaps.get_beatmap_default(beatmap_id).await?,
}; };
let mods = mods.to_mods(mode.unwrap_or(bm.mode))?;
let info = { let info = {
let mode = mode.unwrap_or(bm.mode); let mode = mode.unwrap_or(bm.mode);
env.oppai env.oppai
.get_beatmap(bm.beatmap_id) .get_beatmap(bm.beatmap_id)
.await .await
.and_then(|b| b.get_possible_pp_with(mode, mods))? .and_then(|b| b.get_possible_pp_with(mode, &mods))?
}; };
Ok(Self::Beatmap(Box::new(bm), info, mods)) Ok(Self::Beatmap(Box::new(bm), info, mods))
} }

View file

@ -25,12 +25,15 @@ use youmubot_prelude::announcer::AnnouncerHandler;
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::{ use crate::{
discord::beatmap_cache::BeatmapMetaCache, discord::{
discord::display::ScoreListStyle, beatmap_cache::BeatmapMetaCache,
discord::oppai_cache::{BeatmapCache, BeatmapInfo}, display::ScoreListStyle,
oppai_cache::{BeatmapCache, BeatmapInfo},
},
models::{Beatmap, Mode, Mods, Score, User}, models::{Beatmap, Mode, Mods, Score, User},
mods::UnparsedMods,
request::{BeatmapRequestKind, UserID}, request::{BeatmapRequestKind, UserID},
Client as OsuHttpClient, OsuClient as OsuHttpClient,
}; };
mod announcer; mod announcer;
@ -49,7 +52,7 @@ mod server_rank;
pub(crate) struct OsuClient; pub(crate) struct OsuClient;
impl TypeMapKey for OsuClient { impl TypeMapKey for OsuClient {
type Value = Arc<crate::Client>; type Value = Arc<crate::OsuClient>;
} }
/// The environment for osu! app commands. /// The environment for osu! app commands.
@ -60,7 +63,7 @@ pub struct OsuEnv {
pub(crate) saved_users: OsuSavedUsers, pub(crate) saved_users: OsuSavedUsers,
pub(crate) last_beatmaps: OsuLastBeatmap, pub(crate) last_beatmaps: OsuLastBeatmap,
// clients // clients
pub(crate) client: Arc<crate::Client>, pub(crate) client: Arc<crate::OsuClient>,
pub(crate) oppai: BeatmapCache, pub(crate) oppai: BeatmapCache,
pub(crate) beatmaps: BeatmapMetaCache, pub(crate) beatmaps: BeatmapMetaCache,
} }
@ -199,8 +202,8 @@ pub async fn mania(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode); pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode);
impl BeatmapWithMode { impl BeatmapWithMode {
pub fn short_link(&self, mods: Mods) -> String { pub fn short_link(&self, mods: &Mods) -> String {
self.0.short_link(Some(self.1), Some(mods)) self.0.short_link(Some(self.1), mods)
} }
fn mode(&self) -> Mode { fn mode(&self) -> Mode {
@ -623,14 +626,17 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
match b { match b {
Some((bm, mods_def)) => { Some((bm, mods_def)) => {
let mods = args.find::<Mods>().ok().or(mods_def).unwrap_or(Mods::NOMOD); let mods = match args.find::<UnparsedMods>().ok() {
Some(m) => m.to_mods(bm.mode())?,
None => mods_def.unwrap_or_default(),
};
if beatmapset { if beatmapset {
let beatmapset = env.beatmaps.get_beatmapset(bm.0.beatmapset_id).await?; let beatmapset = env.beatmaps.get_beatmapset(bm.0.beatmapset_id).await?;
display::display_beatmapset( display::display_beatmapset(
ctx, ctx,
beatmapset, beatmapset,
None, None,
Some(mods), mods,
msg, msg,
msg.guild_id, msg.guild_id,
"Here is the beatmapset you requested!", "Here is the beatmapset you requested!",
@ -642,13 +648,13 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.oppai .oppai
.get_beatmap(bm.0.beatmap_id) .get_beatmap(bm.0.beatmap_id)
.await? .await?
.get_possible_pp_with(bm.1, mods)?; .get_possible_pp_with(bm.1, &mods)?;
msg.channel_id msg.channel_id
.send_message( .send_message(
&ctx, &ctx,
CreateMessage::new() CreateMessage::new()
.content("Here is the beatmap you requested!") .content("Here is the beatmap you requested!")
.embed(beatmap_embed(&bm.0, bm.1, mods, info)) .embed(beatmap_embed(&bm.0, bm.1, &mods, info))
.components(vec![beatmap_components(msg.guild_id)]) .components(vec![beatmap_components(msg.guild_id)])
.reference_message(msg), .reference_message(msg),
) )
@ -683,14 +689,18 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
} }
}; };
let mode = bm.1; let mode = bm.1;
let mods = args.find::<Mods>().ok().unwrap_or_default(); let mods = args
.find::<UnparsedMods>()
.ok()
.unwrap_or_default()
.to_mods(mode)?;
let style = args let style = args
.single::<ScoreListStyle>() .single::<ScoreListStyle>()
.unwrap_or(ScoreListStyle::Grid); .unwrap_or(ScoreListStyle::Grid);
let username_arg = args.single::<UsernameArg>().ok(); let username_arg = args.single::<UsernameArg>().ok();
let user = to_user_id_query(username_arg, &env, msg.author.id).await?; let user = to_user_id_query(username_arg, &env, msg.author.id).await?;
let scores = do_check(&env, &bm, mods, &user).await?; let scores = do_check(&env, &bm, &mods, &user).await?;
if scores.is_empty() { if scores.is_empty() {
msg.reply(&ctx, "No scores found").await?; msg.reply(&ctx, "No scores found").await?;
@ -702,7 +712,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
format!( format!(
"Here are the scores by `{}` on `{}`!", "Here are the scores by `{}` on `{}`!",
&user, &user,
bm.short_link(mods) bm.short_link(&mods)
), ),
) )
.await?; .await?;
@ -716,7 +726,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
pub(crate) async fn do_check( pub(crate) async fn do_check(
env: &OsuEnv, env: &OsuEnv,
bm: &BeatmapWithMode, bm: &BeatmapWithMode,
mods: Mods, mods: &Mods,
user: &UserID, user: &UserID,
) -> Result<Vec<Score>> { ) -> Result<Vec<Score>> {
let BeatmapWithMode(b, m) = bm; let BeatmapWithMode(b, m) = bm;
@ -856,7 +866,7 @@ async fn get_user(
.oppai .oppai
.get_beatmap(m.beatmap_id) .get_beatmap(m.beatmap_id)
.await? .await?
.get_info_with(mode, m.mods)?; .get_info_with(mode, &m.mods)?;
Some((m, BeatmapWithMode(beatmap, mode), info)) Some((m, BeatmapWithMode(beatmap, mode), info))
} }
None => None, None => None,
@ -907,7 +917,7 @@ pub(in crate::discord) async fn calculate_weighted_map_length(
let beatmap = cache.get_beatmap(s.beatmap_id, mode).await?; let beatmap = cache.get_beatmap(s.beatmap_id, mode).await?;
Ok(beatmap Ok(beatmap
.difficulty .difficulty
.apply_mods(s.mods, 0.0 /* dont care */) .apply_mods(&s.mods, 0.0 /* dont care */)
.drain_length .drain_length
.as_secs_f64()) as Result<_> .as_secs_f64()) as Result<_>
}) })

View file

@ -74,15 +74,20 @@ impl BeatmapContent {
mode: Mode, mode: Mode,
combo: Option<usize>, combo: Option<usize>,
accuracy: Accuracy, accuracy: Accuracy,
mods: Mods, mods: &Mods,
) -> Result<f64> { ) -> Result<f64> {
let clock = match mods.inner.clock_rate() {
None => bail!("cannot calculate pp for mods: {}", mods),
Some(clock) => clock as f64,
};
let mut perf = self let mut perf = self
.content .content
.performance() .performance()
.mode_or_ignore(mode.into()) .mode_or_ignore(mode.into())
.accuracy(accuracy.into()) .accuracy(accuracy.into())
.misses(accuracy.misses() as u32) .misses(accuracy.misses() as u32)
.mods(mods.bits() as u32); .mods(mods.bits())
.clock_rate(clock);
if let Some(combo) = combo { if let Some(combo) = combo {
perf = perf.combo(combo as u32); perf = perf.combo(combo as u32);
} }
@ -91,12 +96,17 @@ impl BeatmapContent {
} }
/// Get info given mods. /// Get info given mods.
pub fn get_info_with(&self, mode: Mode, mods: Mods) -> Result<BeatmapInfo> { pub fn get_info_with(&self, mode: Mode, mods: &Mods) -> Result<BeatmapInfo> {
let clock = match mods.inner.clock_rate() {
None => bail!("cannot calculate info for mods: {}", mods),
Some(clock) => clock as f64,
};
let attrs = self let attrs = self
.content .content
.performance() .performance()
.mode_or_ignore(mode.into()) .mode_or_ignore(mode.into())
.mods(mods.bits() as u32) .mods(mods.bits())
.clock_rate(clock)
.calculate(); .calculate();
Ok(BeatmapInfo { Ok(BeatmapInfo {
objects: self.content.hit_objects.len(), objects: self.content.hit_objects.len(),
@ -105,7 +115,7 @@ impl BeatmapContent {
}) })
} }
pub fn get_possible_pp_with(&self, mode: Mode, mods: Mods) -> Result<BeatmapInfoWithPP> { pub fn get_possible_pp_with(&self, mode: Mode, mods: &Mods) -> Result<BeatmapInfoWithPP> {
let pp: [f64; 4] = [ let pp: [f64; 4] = [
self.get_pp_from(mode, None, Accuracy::ByValue(95.0, 0), mods)?, self.get_pp_from(mode, None, Accuracy::ByValue(95.0, 0), mods)?,
self.get_pp_from(mode, None, Accuracy::ByValue(98.0, 0), mods)?, self.get_pp_from(mode, None, Accuracy::ByValue(98.0, 0), mods)?,

View file

@ -446,7 +446,7 @@ pub async fn get_leaderboard(
score.count_50, score.count_50,
score.count_miss, score.count_miss,
), ),
score.mods, &score.mods,
) )
.ok() .ok()
.map(|v| (false, v)) .map(|v| (false, v))

View file

@ -14,7 +14,7 @@ pub mod request;
/// 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.
#[derive(Clone)] #[derive(Clone)]
pub struct Client { pub struct OsuClient {
rosu: Arc<rosu_v2::Osu>, rosu: Arc<rosu_v2::Osu>,
user_header_cache: Arc<Mutex<HashMap<u64, Option<UserHeader>>>>, user_header_cache: Arc<Mutex<HashMap<u64, Option<UserHeader>>>>,
@ -30,15 +30,15 @@ pub fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>,
Ok(res) Ok(res)
} }
impl Client { impl OsuClient {
/// Create a new client from the given API key. /// Create a new client from the given API key.
pub async fn new(client_id: u64, client_secret: impl Into<String>) -> Result<Client> { pub async fn new(client_id: u64, client_secret: impl Into<String>) -> Result<OsuClient> {
let rosu = rosu_v2::OsuBuilder::new() let rosu = rosu_v2::OsuBuilder::new()
.client_id(client_id) .client_id(client_id)
.client_secret(client_secret) .client_secret(client_secret)
.build() .build()
.await?; .await?;
Ok(Client { Ok(OsuClient {
rosu: Arc::new(rosu), rosu: Arc::new(rosu),
user_header_cache: Arc::new(Mutex::new(HashMap::new())), user_header_cache: Arc::new(Mutex::new(HashMap::new())),
}) })

View file

@ -1,4 +1,5 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use rosu_v2::prelude::GameModIntermode;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::fmt; use std::fmt;
use std::time::Duration; use std::time::Duration;
@ -85,39 +86,44 @@ impl Difficulty {
// then convert back // then convert back
self.od = (79.0 - (hit_timing - 0.5)) / 6.0; self.od = (79.0 - (hit_timing - 0.5)) / 6.0;
} }
fn apply_length_by_ratio(&mut self, mul: u32, div: u32) { fn apply_length_by_ratio(&mut self, ratio: f64) {
self.bpm = self.bpm / (mul as f64) * (div as f64); // Inverse since bpm increases while time decreases self.bpm /= ratio; // Inverse since bpm increases while time decreases
self.drain_length = self.drain_length * mul / div; self.drain_length = Duration::from_secs_f64(self.drain_length.as_secs_f64() * ratio);
self.total_length = self.total_length * mul / div; self.total_length = Duration::from_secs_f64(self.total_length.as_secs_f64() * ratio);
} }
/// Apply mods to the given difficulty. /// Apply mods to the given difficulty.
/// Note that `stars`, `aim` and `speed` cannot be calculated from this alone. /// Note that `stars`, `aim` and `speed` cannot be calculated from this alone.
pub fn apply_mods(&self, mods: Mods, updated_stars: f64) -> Difficulty { pub fn apply_mods(&self, mods: &Mods, updated_stars: f64) -> Difficulty {
let mut diff = Difficulty { let mut diff = Difficulty {
stars: updated_stars, stars: updated_stars,
..self.clone() ..self.clone()
}; };
// Apply mods one by one // Apply mods one by one
if mods.contains(Mods::EZ) { if mods.inner.contains_intermode(GameModIntermode::Easy) {
diff.apply_everything_by_ratio(0.5); diff.apply_everything_by_ratio(0.5);
} }
if mods.contains(Mods::HR) { if mods.inner.contains_intermode(GameModIntermode::HardRock) {
let old_cs = diff.cs; let old_cs = diff.cs;
diff.apply_everything_by_ratio(1.4); diff.apply_everything_by_ratio(1.4);
// CS is changed by 1.3 tho // CS is changed by 1.3 tho
diff.cs = old_cs * 1.3; diff.cs = old_cs * 1.3;
} }
if mods.contains(Mods::HT) { if let Some(ratio) = mods.inner.clock_rate() {
diff.apply_ar_by_time_ratio(4.0 / 3.0); diff.apply_length_by_ratio(1.0 / ratio as f64);
diff.apply_od_by_time_ratio(4.0 / 3.0); diff.apply_ar_by_time_ratio(1.0 / ratio as f64);
diff.apply_length_by_ratio(4, 3); diff.apply_od_by_time_ratio(1.0 / ratio as f64);
}
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);
} }
// 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::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 diff
} }
@ -126,7 +132,7 @@ impl Difficulty {
pub fn format_info<'a>( pub fn format_info<'a>(
&self, &self,
mode: Mode, mode: Mode,
mods: Mods, mods: &Mods,
original_beatmap: impl Into<Option<&'a Beatmap>> + 'a, original_beatmap: impl Into<Option<&'a Beatmap>> + 'a,
) -> String { ) -> String {
let original_beatmap = original_beatmap.into(); let original_beatmap = original_beatmap.into();
@ -146,7 +152,7 @@ impl Difficulty {
original_beatmap.download_link(BeatmapSite::Bancho), original_beatmap.download_link(BeatmapSite::Bancho),
original_beatmap.download_link(BeatmapSite::Beatconnect), original_beatmap.download_link(BeatmapSite::Beatconnect),
original_beatmap.download_link(BeatmapSite::Chimu), original_beatmap.download_link(BeatmapSite::Chimu),
original_beatmap.short_link(Some(mode), Some(mods)) original_beatmap.short_link(Some(mode), mods)
) )
}) })
.unwrap_or("**Uploaded**".to_owned()), .unwrap_or("**Uploaded**".to_owned()),
@ -405,7 +411,7 @@ impl Beatmap {
} }
/// Return a parsable short link. /// Return a parsable short link.
pub fn short_link(&self, override_mode: Option<Mode>, mods: Option<Mods>) -> String { pub fn short_link(&self, override_mode: Option<Mode>, mods: &Mods) -> String {
format!( format!(
"/b/{}{}{}", "/b/{}{}{}",
self.beatmap_id, self.beatmap_id,
@ -413,8 +419,7 @@ impl Beatmap {
Some(mode) if mode != self.mode => format!("/{}", mode.as_str_new_site()), Some(mode) if mode != self.mode => format!("/{}", mode.as_str_new_site()),
_ => "".to_owned(), _ => "".to_owned(),
}, },
mods.map(|m| format!("{}", m)) mods.strip_lazer(override_mode.unwrap_or(Mode::Std))
.unwrap_or_else(|| "".to_owned()),
) )
} }
@ -587,7 +592,7 @@ impl fmt::Display for Rank {
} }
} }
#[derive(Clone, Debug, Serialize, Deserialize)] #[derive(Clone, Debug)]
pub struct Score { pub struct Score {
pub id: Option<u64>, // No id if you fail pub id: Option<u64>, // No id if you fail
pub user_id: u64, pub user_id: u64,

View file

@ -1,169 +1,439 @@
use serde::{Deserialize, Serialize}; use regex::Regex;
use rosu::{GameModIntermode, GameMods};
use rosu_v2::model::mods as rosu;
use rosu_v2::prelude::GameModsIntermode;
use std::borrow::Cow;
use std::fmt; use std::fmt;
use std::str::FromStr;
use youmubot_prelude::*;
use crate::Mode;
const LAZER_TEXT: &str = "v2"; const LAZER_TEXT: &str = "v2";
bitflags::bitflags! { lazy_static::lazy_static! {
/// The mods available to osu! // Beatmap(set) hooks
#[derive(std::default::Default, Serialize, Deserialize)] static ref MODS: Regex = Regex::new(
pub struct Mods: u64 { // r"(?:https?://)?osu\.ppy\.sh/(?P<link_type>s|b|beatmaps)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>[0123]))?(?:\+(?P<mods>[A-Z]+))?"
const NF = 1 << 0; r"^((\+?)(?P<mods>([A-Za-z0-9][A-Za-z])+))?(@(?P<clock>\d(\.\d+)?)x)?(v2)?$"
const EZ = 1 << 1; ).unwrap();
const TD = 1 << 2; }
const HD = 1 << 3;
const HR = 1 << 4;
const SD = 1 << 5;
const DT = 1 << 6;
const RX = 1 << 7;
const HT = 1 << 8;
const NC = 1 << 9;
const FL = 1 << 10;
const AT = 1 << 11;
const SO = 1 << 12;
const AP = 1 << 13;
const PF = 1 << 14;
const KEY4 = 1 << 15; /* TODO: what are these abbreviated to? */
const KEY5 = 1 << 16;
const KEY6 = 1 << 17;
const KEY7 = 1 << 18;
const KEY8 = 1 << 19;
const FADEIN = 1 << 20;
const RANDOM = 1 << 21;
const CINEMA = 1 << 22;
const TARGET = 1 << 23;
const KEY9 = 1 << 24;
const KEYCOOP = 1 << 25;
const KEY1 = 1 << 26;
const KEY3 = 1 << 27;
const KEY2 = 1 << 28;
const SCOREV2 = 1 << 29;
// Made up flags #[derive(Debug, Clone, PartialEq)]
const LAZER = 1 << 59; pub struct UnparsedMods {
const UNKNOWN = 1 << 60; mods: Cow<'static, str>,
clock: Option<f32>,
}
impl Default for UnparsedMods {
fn default() -> Self {
Self {
mods: "".into(),
clock: None,
}
} }
} }
impl Mods { impl FromStr for UnparsedMods {
pub const NOMOD: Mods = Mods::empty(); type Err = String;
pub const TOUCH_DEVICE: Mods = Self::TD;
pub const NOVIDEO: Mods = Self::TD; /* never forget */ fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
pub const SPEED_CHANGING: Mods = if s.is_empty() {
Mods::from_bits_truncate(Self::DT.bits | Self::HT.bits | Self::NC.bits); return Ok(UnparsedMods::default());
pub const MAP_CHANGING: Mods = }
Mods::from_bits_truncate(Self::HR.bits | Self::EZ.bits | Self::SPEED_CHANGING.bits); let ms = match MODS.captures(s) {
Some(m) => m,
None => return Err(format!("invalid mods: {}", s)),
};
let mods = ms.name("mods").map(|v| v.as_str().to_owned());
if let Some(mods) = &mods {
if GameModsIntermode::try_from_acronyms(mods).is_none() {
return Err(format!("invalid mod sequence: {}", mods));
}
}
Ok(Self {
mods: mods.map(|v| v.into()).unwrap_or("".into()),
clock: ms
.name("clock")
.map(|v| v.as_str().parse::<f32>().unwrap())
.filter(|v| *v > 0.0),
})
}
} }
const MODS_WITH_NAMES: &[(Mods, &str)] = &[ impl UnparsedMods {
(Mods::NF, "NF"), /// Convert to [Mods].
(Mods::EZ, "EZ"), pub fn to_mods(&self, mode: Mode) -> Result<Mods> {
(Mods::TD, "TD"), use rosu_v2::prelude::*;
(Mods::HD, "HD"), let mut mods = Mods::from_str(&self.mods, mode)?;
(Mods::HR, "HR"), if let Some(clock) = self.clock {
(Mods::SD, "SD"), let has_night_day_core = mods.inner.contains_intermode(GameModIntermode::Nightcore)
(Mods::DT, "DT"), || mods.inner.contains_intermode(GameModIntermode::Daycore);
(Mods::RX, "RX"), mods.inner.remove_all_intermode([
(Mods::HT, "HT"), GameModIntermode::Daycore,
(Mods::NC, "NC"), GameModIntermode::Nightcore,
(Mods::FL, "FL"), GameModIntermode::DoubleTime,
(Mods::AT, "AT"), GameModIntermode::HalfTime,
(Mods::SO, "SO"), ]);
(Mods::AP, "AP"), let mut speed_change = Some(clock);
(Mods::PF, "PF"), let adjust_pitch: Option<bool> = None;
(Mods::KEY1, "1K"), if clock < 1.0 {
(Mods::KEY2, "2K"), speed_change = speed_change.filter(|v| *v != 0.75);
(Mods::KEY3, "3K"), mods.inner.insert(if has_night_day_core {
(Mods::KEY4, "4K"), match mode {
(Mods::KEY5, "5K"), Mode::Std => GameMod::DaycoreOsu(DaycoreOsu { speed_change }),
(Mods::KEY6, "6K"), Mode::Taiko => GameMod::DaycoreTaiko(DaycoreTaiko { speed_change }),
(Mods::KEY7, "7K"), Mode::Catch => GameMod::DaycoreCatch(DaycoreCatch { speed_change }),
(Mods::KEY8, "8K"), Mode::Mania => GameMod::DaycoreMania(DaycoreMania { speed_change }),
(Mods::KEY9, "9K"), }
(Mods::UNKNOWN, "??"), } else {
]; match mode {
Mode::Std => GameMod::HalfTimeOsu(HalfTimeOsu {
speed_change,
adjust_pitch,
}),
Mode::Taiko => GameMod::HalfTimeTaiko(HalfTimeTaiko {
speed_change,
adjust_pitch,
}),
Mode::Catch => GameMod::HalfTimeCatch(HalfTimeCatch {
speed_change,
adjust_pitch,
}),
Mode::Mania => GameMod::HalfTimeMania(HalfTimeMania {
speed_change,
adjust_pitch,
}),
}
})
}
if clock > 1.0 {
speed_change = speed_change.filter(|v| *v != 1.5);
mods.inner.insert(if has_night_day_core {
match mode {
Mode::Std => GameMod::NightcoreOsu(NightcoreOsu { speed_change }),
Mode::Taiko => GameMod::NightcoreTaiko(NightcoreTaiko { speed_change }),
Mode::Catch => GameMod::NightcoreCatch(NightcoreCatch { speed_change }),
Mode::Mania => GameMod::NightcoreMania(NightcoreMania { speed_change }),
}
} else {
match mode {
Mode::Std => GameMod::DoubleTimeOsu(DoubleTimeOsu {
speed_change,
adjust_pitch,
}),
Mode::Taiko => GameMod::DoubleTimeTaiko(DoubleTimeTaiko {
speed_change,
adjust_pitch,
}),
Mode::Catch => GameMod::DoubleTimeCatch(DoubleTimeCatch {
speed_change,
adjust_pitch,
}),
Mode::Mania => GameMod::DoubleTimeMania(DoubleTimeMania {
speed_change,
adjust_pitch,
}),
}
})
}
};
Ok(mods)
}
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct Mods {
pub inner: GameMods,
}
impl Mods { impl Mods {
// Return the string length of the string representation of the mods. pub const NOMOD: &'static Mods = &Mods {
pub fn str_len(&self) -> usize { inner: GameMods::new(),
let s = format!("{}", self); };
s.len()
pub fn strip_lazer(&self, mode: Mode) -> Self {
let mut m = self.clone();
m.inner.insert(Self::classic_mod_of(mode));
m
} }
fn classic_mod_of(mode: Mode) -> rosu::GameMod {
match mode {
Mode::Std => rosu::GameMod::ClassicOsu(rosu::generated_mods::ClassicOsu::default()),
Mode::Taiko => {
rosu::GameMod::ClassicTaiko(rosu::generated_mods::ClassicTaiko::default())
}
Mode::Catch => {
rosu::GameMod::ClassicCatch(rosu::generated_mods::ClassicCatch::default())
}
Mode::Mania => {
rosu::GameMod::ClassicMania(rosu::generated_mods::ClassicMania::default())
}
}
}
}
impl From<GameMods> for Mods {
fn from(inner: GameMods) -> Self {
Self { inner }
}
}
// bitflags::bitflags! {
// /// The mods available to osu!
// #[derive(std::default::Default, Serialize, Deserialize)]
// pub struct Mods: u64 {
// const NF = 1 << 0;
// const EZ = 1 << 1;
// const TD = 1 << 2;
// const HD = 1 << 3;
// const HR = 1 << 4;
// const SD = 1 << 5;
// const DT = 1 << 6;
// const RX = 1 << 7;
// const HT = 1 << 8;
// const NC = 1 << 9;
// const FL = 1 << 10;
// const AT = 1 << 11;
// const SO = 1 << 12;
// const AP = 1 << 13;
// const PF = 1 << 14;
// const KEY4 = 1 << 15; /* TODO: what are these abbreviated to? */
// const KEY5 = 1 << 16;
// const KEY6 = 1 << 17;
// const KEY7 = 1 << 18;
// const KEY8 = 1 << 19;
// const FADEIN = 1 << 20;
// const RANDOM = 1 << 21;
// const CINEMA = 1 << 22;
// const TARGET = 1 << 23;
// const KEY9 = 1 << 24;
// const KEYCOOP = 1 << 25;
// const KEY1 = 1 << 26;
// const KEY3 = 1 << 27;
// const KEY2 = 1 << 28;
// const SCOREV2 = 1 << 29;
// // Made up flags
// const LAZER = 1 << 59;
// const UNKNOWN = 1 << 60;
// }
// }
// impl Mods {
// pub const NOMOD: Mods = Mods::empty();
// pub const TOUCH_DEVICE: Mods = Self::TD;
// pub const NOVIDEO: Mods = Self::TD; /* never forget */
// pub const SPEED_CHANGING: Mods =
// Mods::from_bits_truncate(Self::DT.bits | Self::HT.bits | Self::NC.bits);
// pub const MAP_CHANGING: Mods =
// Mods::from_bits_truncate(Self::HR.bits | Self::EZ.bits | Self::SPEED_CHANGING.bits);
// }
// const MODS_WITH_NAMES: &[(Mods, &str)] = &[
// (Mods::NF, "NF"),
// (Mods::EZ, "EZ"),
// (Mods::TD, "TD"),
// (Mods::HD, "HD"),
// (Mods::HR, "HR"),
// (Mods::SD, "SD"),
// (Mods::DT, "DT"),
// (Mods::RX, "RX"),
// (Mods::HT, "HT"),
// (Mods::NC, "NC"),
// (Mods::FL, "FL"),
// (Mods::AT, "AT"),
// (Mods::SO, "SO"),
// (Mods::AP, "AP"),
// (Mods::PF, "PF"),
// (Mods::KEY1, "1K"),
// (Mods::KEY2, "2K"),
// (Mods::KEY3, "3K"),
// (Mods::KEY4, "4K"),
// (Mods::KEY5, "5K"),
// (Mods::KEY6, "6K"),
// (Mods::KEY7, "7K"),
// (Mods::KEY8, "8K"),
// (Mods::KEY9, "9K"),
// (Mods::UNKNOWN, "??"),
// ];
impl Mods {
pub fn bits(&self) -> u32 {
self.inner.bits()
}
pub fn contains(&self, other: &Mods) -> bool {
other
.inner
.iter()
.filter(|v| v.acronym().as_str() != "CL")
.all(|m| self.inner.contains(m))
}
// Format the mods into a string with padded size. // Format the mods into a string with padded size.
pub fn to_string_padded(&self, size: usize) -> String { pub fn to_string_padded(&self, size: usize) -> String {
let s = format!("{}", self); let s = format!("{}", self);
let real_padded = size; let real_padded = size;
format!("{:>mw$}", s, mw = real_padded) format!("{:>mw$}", s, mw = real_padded)
} }
/// Get details on the mods, if they are present.
pub fn details(&self) -> Vec<String> {
use rosu::GameMod::*;
fn fmt_speed_change(
mod_name: &str,
speed_change: &Option<f32>,
adjust_pitch: &Option<bool>,
) -> Option<String> {
if speed_change.is_none() && adjust_pitch.is_none() {
return None;
}
let mut s = format!("**{}**: ", mod_name);
let mut need_comma = false;
if let Some(speed) = speed_change {
s += &format!("speed **{:.2}x**", speed);
need_comma = true;
}
if let Some(true) = adjust_pitch {
if need_comma {
s += ", ";
}
s += "pitch **changed**"
}
Some(s)
}
self.inner
.iter()
.filter_map(|m| match m {
DoubleTimeOsu(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch),
DoubleTimeTaiko(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch),
DoubleTimeCatch(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch),
DoubleTimeMania(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch),
NightcoreOsu(dt) => fmt_speed_change("NC", &dt.speed_change, &None),
NightcoreTaiko(dt) => fmt_speed_change("NC", &dt.speed_change, &None),
NightcoreCatch(dt) => fmt_speed_change("NC", &dt.speed_change, &None),
NightcoreMania(dt) => fmt_speed_change("NC", &dt.speed_change, &None),
HalfTimeOsu(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch),
HalfTimeTaiko(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch),
HalfTimeCatch(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch),
HalfTimeMania(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch),
DaycoreOsu(ht) => fmt_speed_change("DC", &ht.speed_change, &None),
DaycoreTaiko(ht) => fmt_speed_change("DC", &ht.speed_change, &None),
DaycoreCatch(ht) => fmt_speed_change("DC", &ht.speed_change, &None),
DaycoreMania(ht) => fmt_speed_change("DC", &ht.speed_change, &None),
_ => None,
})
.collect()
// let mut res: Vec<String> = vec![];
// for m in &self.inner {
// match m {
// DoubleTimeOsu(dt) =>
// }
// }
// res
}
} }
impl std::str::FromStr for Mods { impl Mods {
type Err = String; pub fn from_str(mut s: &str, mode: Mode) -> Result<Self> {
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
let mut res = Self::default();
// Strip leading + // Strip leading +
if s.starts_with('+') { if s.starts_with('+') {
s = &s[1..]; s = &s[1..];
} }
while s.len() >= 2 { let intermode =
let (m, nw) = s.split_at(2); GameModsIntermode::try_from_acronyms(s).ok_or_else(|| error!("Invalid mods: {}", s))?;
s = nw; let mut inner = intermode
match &m.to_uppercase()[..] { .try_with_mode(mode.into())
"NF" => res |= Mods::NF, .ok_or_else(|| error!("Invalid mods for `{}`: {}", mode, intermode))?;
"EZ" => res |= Mods::EZ, // Always add classic mod to `inner`
"TD" => res |= Mods::TD, inner.insert(Self::classic_mod_of(mode));
"HD" => res |= Mods::HD, if !inner.is_valid() {
"HR" => res |= Mods::HR, return Err(error!("Incompatible mods found: {}", inner));
"SD" => res |= Mods::SD,
"DT" => res |= Mods::DT,
"RX" => res |= Mods::RX,
"HT" => res |= Mods::HT,
"NC" => res |= Mods::NC | Mods::DT,
"FL" => res |= Mods::FL,
"AT" => res |= Mods::AT,
"SO" => res |= Mods::SO,
"AP" => res |= Mods::AP,
"PF" => res |= Mods::PF | Mods::SD,
"1K" => res |= Mods::KEY1,
"2K" => res |= Mods::KEY2,
"3K" => res |= Mods::KEY3,
"4K" => res |= Mods::KEY4,
"5K" => res |= Mods::KEY5,
"6K" => res |= Mods::KEY6,
"7K" => res |= Mods::KEY7,
"8K" => res |= Mods::KEY8,
"9K" => res |= Mods::KEY9,
"??" => res |= Mods::UNKNOWN,
v => return Err(format!("{} is not a valid mod", v)),
}
}
if !s.is_empty() {
Err("String of odd length is not a mod string".to_owned())
} else {
Ok(res)
} }
Ok(Self { inner })
// let mut res = GameModsIntermode::default();
// while s.len() >= 2 {
// let (m, nw) = s.split_at(2);
// s = nw;
// match &m.to_uppercase()[..] {
// "NF" => res.insert(GameModIntermode::NoFail),
// "EZ" => res.insert(GameModIntermode::Easy),
// "TD" => res.insert(GameModIntermode::TouchDevice),
// "HD" => res.insert(GameModIntermode::Hidden),
// "HR" => res.insert(GameModIntermode::HardRock),
// "SD" => res.insert(GameModIntermode::SuddenDeath),
// "DT" => res.insert(GameModIntermode::DoubleTime),
// "RX" => res.insert(GameModIntermode::Relax),
// "HT" => res.insert(GameModIntermode::HalfTime),
// "NC" => res.insert(GameModIntermode::Nightcore),
// "FL" => res.insert(GameModIntermode::Flashlight),
// "AT" => res.insert(GameModIntermode::Autopilot),
// "SO" => res.insert(GameModIntermode::SpunOut),
// "AP" => res.insert(GameModIntermode::Autoplay),
// "PF" => res.insert(GameModIntermode::Perfect),
// "1K" => res.insert(GameModIntermode::OneKey),
// "2K" => res.insert(GameModIntermode::TwoKeys),
// "3K" => res.insert(GameModIntermode::ThreeKeys),
// "4K" => res.insert(GameModIntermode::FourKeys),
// "5K" => res.insert(GameModIntermode::FiveKeys),
// "6K" => res.insert(GameModIntermode::SixKeys),
// "7K" => res.insert(GameModIntermode::SevenKeys),
// "8K" => res.insert(GameModIntermode::EightKeys),
// "9K" => res.insert(GameModIntermode::NineKeys),
// v => return Err(format!("{} is not a valid mod", v)),
// }
// }
// if !s.is_empty() {
// Err("String of odd length is not a mod string".to_owned())
// } else {
// Ok(Mods { inner: res })
// }
} }
} }
impl fmt::Display for Mods { impl fmt::Display for Mods {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
if !(*self & (Mods::all() ^ Mods::LAZER)).is_empty() { let is_lazer = !self.inner.contains_intermode(GameModIntermode::Classic);
write!(f, "+")?; let mods = if !is_lazer {
for p in MODS_WITH_NAMES.iter() { let mut v = self.inner.clone();
if !self.contains(p.0) { v.remove_intermode(GameModIntermode::Classic);
continue; Cow::Owned(v)
} } else {
match p.0 { Cow::Borrowed(&self.inner)
Mods::DT if self.contains(Mods::NC) => continue, };
Mods::SD if self.contains(Mods::PF) => continue, if !mods.is_empty() {
_ => (), write!(f, "+{}", mods)?;
}; }
write!(f, "{}", p.1)?; if let Some(clock) = mods.clock_rate() {
if clock != 1.0 && clock != 1.5 && clock != 0.75 {
write!(f, "@{:.2}x", clock)?;
} }
} }
if self.contains(Mods::LAZER) { if is_lazer {
write!(f, "{}", LAZER_TEXT)?; write!(f, "{}", LAZER_TEXT)?;
} }
Ok(()) Ok(())
// if !(*self & (Mods::all() ^ Mods::LAZER)).is_empty() {
// write!(f, "+")?;
// for p in MODS_WITH_NAMES.iter() {
// if !self.contains(p.0) {
// continue;
// }
// match p.0 {
// Mods::DT if self.contains(Mods::NC) => continue,
// Mods::SD if self.contains(Mods::PF) => continue,
// _ => (),
// };
// write!(f, "{}", p.1)?;
// }
// }
// if self.contains(Mods::LAZER) {
// write!(f, "{}", LAZER_TEXT)?;
// }
// Ok(())
} }
} }

View file

@ -1,7 +1,4 @@
use rosu_v2::model::{ use rosu_v2::model::{self as rosu};
self as rosu,
mods::{GameModIntermode, GameModsIntermode},
};
use super::*; use super::*;
@ -244,180 +241,180 @@ impl From<rosu::Grade> for Rank {
} }
} }
impl From<Mods> for rosu::mods::GameModsIntermode { // impl From<Mods> for rosu::mods::GameModsIntermode {
fn from(value: Mods) -> Self { // fn from(value: Mods) -> Self {
let mut res = GameModsIntermode::new(); // let mut res = GameModsIntermode::new();
const MOD_MAP: &[(Mods, GameModIntermode)] = &[ // const MOD_MAP: &[(Mods, GameModIntermode)] = &[
(Mods::NF, GameModIntermode::NoFail), // (Mods::NF, GameModIntermode::NoFail),
(Mods::EZ, GameModIntermode::Easy), // (Mods::EZ, GameModIntermode::Easy),
(Mods::TD, GameModIntermode::TouchDevice), // (Mods::TD, GameModIntermode::TouchDevice),
(Mods::HD, GameModIntermode::Hidden), // (Mods::HD, GameModIntermode::Hidden),
(Mods::HR, GameModIntermode::HardRock), // (Mods::HR, GameModIntermode::HardRock),
(Mods::SD, GameModIntermode::SuddenDeath), // (Mods::SD, GameModIntermode::SuddenDeath),
(Mods::DT, GameModIntermode::DoubleTime), // (Mods::DT, GameModIntermode::DoubleTime),
(Mods::RX, GameModIntermode::Relax), // (Mods::RX, GameModIntermode::Relax),
(Mods::HT, GameModIntermode::HalfTime), // (Mods::HT, GameModIntermode::HalfTime),
(Mods::NC, GameModIntermode::Nightcore), // (Mods::NC, GameModIntermode::Nightcore),
(Mods::FL, GameModIntermode::Flashlight), // (Mods::FL, GameModIntermode::Flashlight),
(Mods::AT, GameModIntermode::Autoplay), // (Mods::AT, GameModIntermode::Autoplay),
(Mods::SO, GameModIntermode::SpunOut), // (Mods::SO, GameModIntermode::SpunOut),
(Mods::AP, GameModIntermode::Autopilot), // (Mods::AP, GameModIntermode::Autopilot),
(Mods::PF, GameModIntermode::Perfect), // (Mods::PF, GameModIntermode::Perfect),
(Mods::KEY1, GameModIntermode::OneKey), // (Mods::KEY1, GameModIntermode::OneKey),
(Mods::KEY2, GameModIntermode::TwoKeys), // (Mods::KEY2, GameModIntermode::TwoKeys),
(Mods::KEY3, GameModIntermode::ThreeKeys), // (Mods::KEY3, GameModIntermode::ThreeKeys),
(Mods::KEY4, GameModIntermode::FourKeys), // (Mods::KEY4, GameModIntermode::FourKeys),
(Mods::KEY5, GameModIntermode::FiveKeys), // (Mods::KEY5, GameModIntermode::FiveKeys),
(Mods::KEY6, GameModIntermode::SixKeys), // (Mods::KEY6, GameModIntermode::SixKeys),
(Mods::KEY7, GameModIntermode::SevenKeys), // (Mods::KEY7, GameModIntermode::SevenKeys),
(Mods::KEY8, GameModIntermode::EightKeys), // (Mods::KEY8, GameModIntermode::EightKeys),
(Mods::KEY9, GameModIntermode::NineKeys), // (Mods::KEY9, GameModIntermode::NineKeys),
]; // ];
for (m1, m2) in MOD_MAP { // for (m1, m2) in MOD_MAP {
if value.contains(*m1) { // if value.contains(*m1) {
res.insert(*m2); // res.insert(*m2);
} // }
} // }
if !value.contains(Mods::LAZER) { // if !value.contains(Mods::LAZER) {
res.insert(GameModIntermode::Classic); // res.insert(GameModIntermode::Classic);
} // }
res // res
} // }
} // }
impl From<rosu::mods::GameModsIntermode> for Mods { // impl From<rosu::mods::GameModsIntermode> for Mods {
fn from(value: rosu_v2::prelude::GameModsIntermode) -> Self { // fn from(value: rosu_v2::prelude::GameModsIntermode) -> Self {
let init = if value.contains(GameModIntermode::Classic) { // let init = if value.contains(GameModIntermode::Classic) {
Mods::NOMOD // Mods::NOMOD
} else { // } else {
Mods::LAZER // Mods::LAZER
}; // };
value // value
.into_iter() // .into_iter()
.map(|m| match m { // .map(|m| match m {
GameModIntermode::NoFail => Mods::NF, // GameModIntermode::NoFail => Mods::NF,
GameModIntermode::Easy => Mods::EZ, // GameModIntermode::Easy => Mods::EZ,
GameModIntermode::TouchDevice => Mods::TD, // GameModIntermode::TouchDevice => Mods::TD,
GameModIntermode::Hidden => Mods::HD, // GameModIntermode::Hidden => Mods::HD,
GameModIntermode::HardRock => Mods::HR, // GameModIntermode::HardRock => Mods::HR,
GameModIntermode::SuddenDeath => Mods::SD, // GameModIntermode::SuddenDeath => Mods::SD,
GameModIntermode::DoubleTime => Mods::DT, // GameModIntermode::DoubleTime => Mods::DT,
GameModIntermode::Relax => Mods::RX, // GameModIntermode::Relax => Mods::RX,
GameModIntermode::HalfTime => Mods::HT, // GameModIntermode::HalfTime => Mods::HT,
GameModIntermode::Nightcore => Mods::DT | Mods::NC, // GameModIntermode::Nightcore => Mods::DT | Mods::NC,
GameModIntermode::Flashlight => Mods::FL, // GameModIntermode::Flashlight => Mods::FL,
GameModIntermode::Autoplay => Mods::AT, // GameModIntermode::Autoplay => Mods::AT,
GameModIntermode::SpunOut => Mods::SO, // GameModIntermode::SpunOut => Mods::SO,
GameModIntermode::Autopilot => Mods::AP, // GameModIntermode::Autopilot => Mods::AP,
GameModIntermode::Perfect => Mods::SD | Mods::PF, // GameModIntermode::Perfect => Mods::SD | Mods::PF,
GameModIntermode::OneKey => Mods::KEY1, // GameModIntermode::OneKey => Mods::KEY1,
GameModIntermode::TwoKeys => Mods::KEY2, // GameModIntermode::TwoKeys => Mods::KEY2,
GameModIntermode::ThreeKeys => Mods::KEY3, // GameModIntermode::ThreeKeys => Mods::KEY3,
GameModIntermode::FourKeys => Mods::KEY4, // GameModIntermode::FourKeys => Mods::KEY4,
GameModIntermode::FiveKeys => Mods::KEY5, // GameModIntermode::FiveKeys => Mods::KEY5,
GameModIntermode::SixKeys => Mods::KEY6, // GameModIntermode::SixKeys => Mods::KEY6,
GameModIntermode::SevenKeys => Mods::KEY7, // GameModIntermode::SevenKeys => Mods::KEY7,
GameModIntermode::EightKeys => Mods::KEY8, // GameModIntermode::EightKeys => Mods::KEY8,
GameModIntermode::NineKeys => Mods::KEY9, // GameModIntermode::NineKeys => Mods::KEY9,
GameModIntermode::Classic => Mods::NOMOD, // GameModIntermode::Classic => Mods::NOMOD,
_ => Mods::UNKNOWN, // _ => Mods::UNKNOWN,
}) // })
.fold(init, |a, b| a | b) // .fold(init, |a, b| a | b)
// Mods::from_bits_truncate(value.bits() as u64) // // Mods::from_bits_truncate(value.bits() as u64)
} // }
} // }
impl From<rosu::mods::GameMods> for Mods { // impl From<rosu::mods::GameMods> for Mods {
fn from(value: rosu::mods::GameMods) -> Self { // fn from(value: rosu::mods::GameMods) -> Self {
let unknown = // let unknown =
rosu::mods::GameModIntermode::Unknown(rosu_v2::prelude::UnknownMod::default()); // rosu::mods::GameModIntermode::Unknown(rosu_v2::prelude::UnknownMod::default());
value // value
.iter() // .iter()
.cloned() // .cloned()
.map(|m| match m { // .map(|m| match m {
rosu::mods::GameMod::HalfTimeOsu(ht) // rosu::mods::GameMod::HalfTimeOsu(ht)
if ht.speed_change.is_some_and(|v| v != 0.75) => // if ht.speed_change.is_some_and(|v| v != 0.75) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::DaycoreOsu(dc) // rosu::mods::GameMod::DaycoreOsu(dc)
if dc.speed_change.is_some_and(|v| v != 0.75) => // if dc.speed_change.is_some_and(|v| v != 0.75) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::DaycoreOsu(_) => rosu::mods::GameModIntermode::HalfTime, // rosu::mods::GameMod::DaycoreOsu(_) => rosu::mods::GameModIntermode::HalfTime,
rosu::mods::GameMod::DoubleTimeOsu(dt) // rosu::mods::GameMod::DoubleTimeOsu(dt)
if dt.speed_change.is_some_and(|v| v != 1.5) => // if dt.speed_change.is_some_and(|v| v != 1.5) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::NightcoreOsu(nc) // rosu::mods::GameMod::NightcoreOsu(nc)
if nc.speed_change.is_some_and(|v| v != 1.5) => // if nc.speed_change.is_some_and(|v| v != 1.5) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::HalfTimeTaiko(ht) // rosu::mods::GameMod::HalfTimeTaiko(ht)
if ht.speed_change.is_some_and(|v| v != 0.75) => // if ht.speed_change.is_some_and(|v| v != 0.75) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::DaycoreTaiko(dc) // rosu::mods::GameMod::DaycoreTaiko(dc)
if dc.speed_change.is_some_and(|v| v != 0.75) => // if dc.speed_change.is_some_and(|v| v != 0.75) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::DaycoreTaiko(_) => rosu::mods::GameModIntermode::HalfTime, // rosu::mods::GameMod::DaycoreTaiko(_) => rosu::mods::GameModIntermode::HalfTime,
rosu::mods::GameMod::DoubleTimeTaiko(dt) // rosu::mods::GameMod::DoubleTimeTaiko(dt)
if dt.speed_change.is_some_and(|v| v != 1.5) => // if dt.speed_change.is_some_and(|v| v != 1.5) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::NightcoreTaiko(nc) // rosu::mods::GameMod::NightcoreTaiko(nc)
if nc.speed_change.is_some_and(|v| v != 1.5) => // if nc.speed_change.is_some_and(|v| v != 1.5) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::HalfTimeCatch(ht) // rosu::mods::GameMod::HalfTimeCatch(ht)
if ht.speed_change.is_some_and(|v| v != 0.75) => // if ht.speed_change.is_some_and(|v| v != 0.75) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::DaycoreCatch(dc) // rosu::mods::GameMod::DaycoreCatch(dc)
if dc.speed_change.is_some_and(|v| v != 0.75) => // if dc.speed_change.is_some_and(|v| v != 0.75) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::DaycoreCatch(_) => rosu::mods::GameModIntermode::HalfTime, // rosu::mods::GameMod::DaycoreCatch(_) => rosu::mods::GameModIntermode::HalfTime,
rosu::mods::GameMod::DoubleTimeCatch(dt) // rosu::mods::GameMod::DoubleTimeCatch(dt)
if dt.speed_change.is_some_and(|v| v != 1.5) => // if dt.speed_change.is_some_and(|v| v != 1.5) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::NightcoreCatch(nc) // rosu::mods::GameMod::NightcoreCatch(nc)
if nc.speed_change.is_some_and(|v| v != 1.5) => // if nc.speed_change.is_some_and(|v| v != 1.5) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::HalfTimeMania(ht) // rosu::mods::GameMod::HalfTimeMania(ht)
if ht.speed_change.is_some_and(|v| v != 0.75) => // if ht.speed_change.is_some_and(|v| v != 0.75) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::DaycoreMania(dc) // rosu::mods::GameMod::DaycoreMania(dc)
if dc.speed_change.is_some_and(|v| v != 0.75) => // if dc.speed_change.is_some_and(|v| v != 0.75) =>
{ // {
unknown // unknown
} // }
rosu::mods::GameMod::DaycoreMania(_) => rosu::mods::GameModIntermode::HalfTime, // rosu::mods::GameMod::DaycoreMania(_) => rosu::mods::GameModIntermode::HalfTime,
rosu::mods::GameMod::DoubleTimeMania(dt) // rosu::mods::GameMod::DoubleTimeMania(dt)
if dt.speed_change.is_some_and(|v| v != 1.5) => // if dt.speed_change.is_some_and(|v| v != 1.5) =>
{ // {
unknown // unknown
} // }
_ => m.intermode(), // _ => m.intermode(),
}) // })
.collect::<GameModsIntermode>() // .collect::<GameModsIntermode>()
.into() // .into()
} // }
} // }

View file

@ -1,7 +1,7 @@
use core::fmt; use core::fmt;
use crate::models::{Mode, Mods}; use crate::models::{Mode, Mods};
use crate::Client; use crate::OsuClient;
use rosu_v2::error::OsuError; use rosu_v2::error::OsuError;
use youmubot_prelude::*; use youmubot_prelude::*;
@ -58,6 +58,7 @@ pub mod builders {
use crate::models; use crate::models;
use super::OsuClient;
use super::*; use super::*;
/// A builder for a Beatmap request. /// A builder for a Beatmap request.
pub struct BeatmapRequestBuilder { pub struct BeatmapRequestBuilder {
@ -82,7 +83,7 @@ pub mod builders {
self self
} }
pub(crate) async fn build(self, client: &Client) -> Result<Vec<models::Beatmap>> { pub(crate) async fn build(self, client: &OsuClient) -> Result<Vec<models::Beatmap>> {
Ok(match self.kind { Ok(match self.kind {
BeatmapRequestKind::Beatmap(id) => { BeatmapRequestKind::Beatmap(id) => {
match handle_not_found(client.rosu.beatmap().map_id(id as u32).await)? { match handle_not_found(client.rosu.beatmap().map_id(id as u32).await)? {
@ -141,7 +142,7 @@ pub mod builders {
self self
} }
pub(crate) async fn build(self, client: &Client) -> Result<Option<models::User>> { pub(crate) async fn build(self, client: &OsuClient) -> Result<Option<models::User>> {
let mut r = client.rosu.user(self.user); let mut r = client.rosu.user(self.user);
if let Some(mode) = self.mode { if let Some(mode) = self.mode {
r = r.mode(mode.into()); r = r.mode(mode.into());
@ -154,7 +155,7 @@ pub mod builders {
- time::Duration::DAY * self.event_days.unwrap_or(31); - time::Duration::DAY * self.event_days.unwrap_or(31);
let mut events = handle_not_found(client.rosu.recent_activity(user.user_id).await)? let mut events = handle_not_found(client.rosu.recent_activity(user.user_id).await)?
.unwrap_or(vec![]); .unwrap_or(vec![]);
events.retain(|e| (now <= e.created_at)); events.retain(|e: &rosu_v2::model::event::Event| (now <= e.created_at));
let stats = user.statistics.take().unwrap(); let stats = user.statistics.take().unwrap();
Ok(Some(models::User::from_rosu(user, stats, events))) Ok(Some(models::User::from_rosu(user, stats, events)))
} }
@ -199,31 +200,29 @@ pub mod builders {
self self
} }
pub(crate) async fn build(self, client: &Client) -> Result<Vec<models::Score>> { pub(crate) async fn build(self, osu: &OsuClient) -> Result<Vec<models::Score>> {
let scores = handle_not_found(match self.user { let scores = handle_not_found(match self.user {
Some(user) => { Some(user) => {
let mut r = client let mut r = osu.rosu.beatmap_user_scores(self.beatmap_id as u32, user);
.rosu
.beatmap_user_scores(self.beatmap_id as u32, user);
if let Some(mode) = self.mode { if let Some(mode) = self.mode {
r = r.mode(mode.into()); r = r.mode(mode.into());
} }
match self.mods { match self.mods {
Some(mods) => r.await.map(|mut ss| { Some(mods) => r.await.map(|mut ss| {
let mods = GameModsIntermode::from(mods); // let mods = GameModsIntermode::from(mods.inner);
ss.retain(|s| mods.iter().all(|m| s.mods.contains_intermode(m))); ss.retain(|s| Mods::from(s.mods.clone()).contains(&mods));
ss ss
}), }),
None => r.await, None => r.await,
} }
} }
None => { None => {
let mut r = client.rosu.beatmap_scores(self.beatmap_id as u32).global(); let mut r = osu.rosu.beatmap_scores(self.beatmap_id as u32).global();
if let Some(mode) = self.mode { if let Some(mode) = self.mode {
r = r.mode(mode.into()); r = r.mode(mode.into());
} }
if let Some(mods) = self.mods { if let Some(mods) = self.mods {
r = r.mods(GameModsIntermode::from(mods)); r = r.mods(GameModsIntermode::from(mods.inner));
} }
if let Some(limit) = self.limit { if let Some(limit) = self.limit {
r = r.limit(limit as u32); r = r.limit(limit as u32);
@ -268,7 +267,7 @@ pub mod builders {
self self
} }
pub(crate) async fn build(self, client: &Client) -> Result<Vec<models::Score>> { pub(crate) async fn build(self, client: &OsuClient) -> Result<Vec<models::Score>> {
let scores = handle_not_found({ let scores = handle_not_found({
let mut r = client.rosu.user_scores(self.user); let mut r = client.rosu.user_scores(self.user);
r = match self.score_type { r = match self.score_type {

View file

@ -142,7 +142,6 @@ impl AnnouncerHandler {
} }
pub fn run(self, client: &Client) -> AnnouncerRunner { pub fn run(self, client: &Client) -> AnnouncerRunner {
AnnouncerRunner { AnnouncerRunner {
cache_http: CacheAndHttp::from_client(client), cache_http: CacheAndHttp::from_client(client),
data: client.data.clone(), data: client.data.clone(),

View file

@ -39,8 +39,6 @@ pub async fn setup_prelude(
// Set up the SQL client. // Set up the SQL client.
data.insert::<crate::SQLClient>(sql_pool.clone()); data.insert::<crate::SQLClient>(sql_pool.clone());
Env { Env {
http: http_client, http: http_client,
sql: sql_pool, sql: sql_pool,