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": {
"lastModified": 1718730147,
"narHash": "sha256-QmD6B6FYpuoCqu6ZuPJH896ItNquDkn0ulQlOn4ykN8=",
"lastModified": 1724377159,
"narHash": "sha256-ixjje1JO8ucKT41hs6n2NCde1Vc0+Zc2p2gUbJpCsMw=",
"owner": "ipetkov",
"repo": "crane",
"rev": "32c21c29b034d0a93fdb2379d6fabc40fc3d0e6c",
"rev": "3e47b7a86c19142bd3675da49d6acef488b4dac1",
"type": "github"
},
"original": {
@ -40,11 +40,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1718530797,
"narHash": "sha256-pup6cYwtgvzDpvpSCFh1TEUjw2zkNpk8iolbKnyFmmU=",
"lastModified": 1724224976,
"narHash": "sha256-Z/ELQhrSd7bMzTO8r7NZgi9g5emh+aRKoCdaAv5fiO0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "b60ebf54c15553b393d144357375ea956f89e9a9",
"rev": "c374d94f1536013ca8e92341b540eba4c22f9c62",
"type": "github"
},
"original": {
@ -58,7 +58,28 @@
"inputs": {
"crane": "crane",
"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": {

View file

@ -7,16 +7,24 @@
inputs.nixpkgs.follows = "nixpkgs";
};
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
};
};
nixConfig = {
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
(system:
let
pkgs = import nixpkgs { inherit system; };
craneLib = inputs.crane.mkLib pkgs;
pkgs = import nixpkgs
{
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
rec {
packages.youmubot = pkgs.callPackage ./package.nix { inherit craneLib; };
@ -35,7 +43,7 @@
{
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; [
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,
models::{Mode, Score, User, UserEventRank},
request::UserID,
Client as Osu,
OsuClient as Osu,
};
use super::db::OsuUser;

View file

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

View file

@ -211,7 +211,7 @@ mod scores {
let beatmap = meta_cache.get_beatmap(play.beatmap_id, mode).await?;
let info = {
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>)>
})
@ -236,7 +236,7 @@ mod scores {
p.count_50,
p.count_miss,
),
p.mods,
&p.mods,
)
.ok()
.map(|pp| format!("{:.2}[?]", pp))
@ -291,7 +291,7 @@ mod scores {
beatmap.artist,
beatmap.title,
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())
@ -359,13 +359,11 @@ mod beatmapset {
ctx: &Context,
beatmapset: Vec<Beatmap>,
mode: Option<Mode>,
mods: Option<Mods>,
mods: Mods,
reply_to: &Message,
guild_id: Option<GuildId>,
message: impl AsRef<str>,
) -> Result<bool> {
let mods = mods.unwrap_or(Mods::NOMOD);
if beatmapset.is_empty() {
return Ok(false);
}
@ -405,7 +403,7 @@ mod beatmapset {
env.oppai
.get_beatmap(b.beatmap_id)
.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(
map,
self.mode.unwrap_or(map.mode),
self.mods,
&self.mods,
info,
)
.footer({

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -74,15 +74,20 @@ impl BeatmapContent {
mode: Mode,
combo: Option<usize>,
accuracy: Accuracy,
mods: Mods,
mods: &Mods,
) -> 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
.content
.performance()
.mode_or_ignore(mode.into())
.accuracy(accuracy.into())
.misses(accuracy.misses() as u32)
.mods(mods.bits() as u32);
.mods(mods.bits())
.clock_rate(clock);
if let Some(combo) = combo {
perf = perf.combo(combo as u32);
}
@ -91,12 +96,17 @@ impl BeatmapContent {
}
/// 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
.content
.performance()
.mode_or_ignore(mode.into())
.mods(mods.bits() as u32)
.mods(mods.bits())
.clock_rate(clock)
.calculate();
Ok(BeatmapInfo {
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] = [
self.get_pp_from(mode, None, Accuracy::ByValue(95.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_miss,
),
score.mods,
&score.mods,
)
.ok()
.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.
#[derive(Clone)]
pub struct Client {
pub struct OsuClient {
rosu: Arc<rosu_v2::Osu>,
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)
}
impl Client {
impl OsuClient {
/// 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()
.client_id(client_id)
.client_secret(client_secret)
.build()
.await?;
Ok(Client {
Ok(OsuClient {
rosu: Arc::new(rosu),
user_header_cache: Arc::new(Mutex::new(HashMap::new())),
})

View file

@ -1,4 +1,5 @@
use chrono::{DateTime, Utc};
use rosu_v2::prelude::GameModIntermode;
use serde::{Deserialize, Serialize};
use std::fmt;
use std::time::Duration;
@ -85,39 +86,44 @@ impl Difficulty {
// 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.bpm = self.bpm / (mul as f64) * (div as f64); // Inverse since bpm increases while time decreases
self.drain_length = self.drain_length * mul / div;
self.total_length = self.total_length * mul / div;
fn apply_length_by_ratio(&mut self, ratio: f64) {
self.bpm /= ratio; // Inverse since bpm increases while time decreases
self.drain_length = Duration::from_secs_f64(self.drain_length.as_secs_f64() * ratio);
self.total_length = Duration::from_secs_f64(self.total_length.as_secs_f64() * ratio);
}
/// 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, updated_stars: f64) -> Difficulty {
pub fn apply_mods(&self, mods: &Mods, updated_stars: f64) -> Difficulty {
let mut diff = Difficulty {
stars: updated_stars,
..self.clone()
};
// Apply mods one by one
if mods.contains(Mods::EZ) {
if mods.inner.contains_intermode(GameModIntermode::Easy) {
diff.apply_everything_by_ratio(0.5);
}
if mods.contains(Mods::HR) {
if mods.inner.contains_intermode(GameModIntermode::HardRock) {
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::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);
if let Some(ratio) = mods.inner.clock_rate() {
diff.apply_length_by_ratio(1.0 / ratio as f64);
diff.apply_ar_by_time_ratio(1.0 / ratio as f64);
diff.apply_od_by_time_ratio(1.0 / ratio as f64);
}
// 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
}
@ -126,7 +132,7 @@ impl Difficulty {
pub fn format_info<'a>(
&self,
mode: Mode,
mods: Mods,
mods: &Mods,
original_beatmap: impl Into<Option<&'a Beatmap>> + 'a,
) -> String {
let original_beatmap = original_beatmap.into();
@ -146,7 +152,7 @@ impl Difficulty {
original_beatmap.download_link(BeatmapSite::Bancho),
original_beatmap.download_link(BeatmapSite::Beatconnect),
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()),
@ -405,7 +411,7 @@ impl Beatmap {
}
/// 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!(
"/b/{}{}{}",
self.beatmap_id,
@ -413,8 +419,7 @@ impl Beatmap {
Some(mode) if mode != self.mode => format!("/{}", mode.as_str_new_site()),
_ => "".to_owned(),
},
mods.map(|m| format!("{}", m))
.unwrap_or_else(|| "".to_owned()),
mods.strip_lazer(override_mode.unwrap_or(Mode::Std))
)
}
@ -587,7 +592,7 @@ impl fmt::Display for Rank {
}
}
#[derive(Clone, Debug, Serialize, Deserialize)]
#[derive(Clone, Debug)]
pub struct Score {
pub id: Option<u64>, // No id if you fail
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::str::FromStr;
use youmubot_prelude::*;
use crate::Mode;
const LAZER_TEXT: &str = "v2";
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;
lazy_static::lazy_static! {
// Beatmap(set) hooks
static ref MODS: Regex = Regex::new(
// r"(?:https?://)?osu\.ppy\.sh/(?P<link_type>s|b|beatmaps)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>[0123]))?(?:\+(?P<mods>[A-Z]+))?"
r"^((\+?)(?P<mods>([A-Za-z0-9][A-Za-z])+))?(@(?P<clock>\d(\.\d+)?)x)?(v2)?$"
).unwrap();
}
// Made up flags
const LAZER = 1 << 59;
const UNKNOWN = 1 << 60;
#[derive(Debug, Clone, PartialEq)]
pub struct UnparsedMods {
mods: Cow<'static, str>,
clock: Option<f32>,
}
impl Default for UnparsedMods {
fn default() -> Self {
Self {
mods: "".into(),
clock: None,
}
}
}
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);
impl FromStr for UnparsedMods {
type Err = String;
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
if s.is_empty() {
return Ok(UnparsedMods::default());
}
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)] = &[
(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 UnparsedMods {
/// Convert to [Mods].
pub fn to_mods(&self, mode: Mode) -> Result<Mods> {
use rosu_v2::prelude::*;
let mut mods = Mods::from_str(&self.mods, mode)?;
if let Some(clock) = self.clock {
let has_night_day_core = mods.inner.contains_intermode(GameModIntermode::Nightcore)
|| mods.inner.contains_intermode(GameModIntermode::Daycore);
mods.inner.remove_all_intermode([
GameModIntermode::Daycore,
GameModIntermode::Nightcore,
GameModIntermode::DoubleTime,
GameModIntermode::HalfTime,
]);
let mut speed_change = Some(clock);
let adjust_pitch: Option<bool> = None;
if clock < 1.0 {
speed_change = speed_change.filter(|v| *v != 0.75);
mods.inner.insert(if has_night_day_core {
match mode {
Mode::Std => GameMod::DaycoreOsu(DaycoreOsu { speed_change }),
Mode::Taiko => GameMod::DaycoreTaiko(DaycoreTaiko { speed_change }),
Mode::Catch => GameMod::DaycoreCatch(DaycoreCatch { speed_change }),
Mode::Mania => GameMod::DaycoreMania(DaycoreMania { speed_change }),
}
} 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 {
// Return the string length of the string representation of the mods.
pub fn str_len(&self) -> usize {
let s = format!("{}", self);
s.len()
pub const NOMOD: &'static Mods = &Mods {
inner: GameMods::new(),
};
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.
pub fn to_string_padded(&self, size: usize) -> String {
let s = format!("{}", self);
let real_padded = size;
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 {
type Err = String;
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
let mut res = Self::default();
impl Mods {
pub fn from_str(mut s: &str, mode: Mode) -> Result<Self> {
// Strip leading +
if s.starts_with('+') {
s = &s[1..];
}
while s.len() >= 2 {
let (m, nw) = s.split_at(2);
s = nw;
match &m.to_uppercase()[..] {
"NF" => res |= Mods::NF,
"EZ" => res |= Mods::EZ,
"TD" => res |= Mods::TD,
"HD" => res |= Mods::HD,
"HR" => res |= Mods::HR,
"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)
let intermode =
GameModsIntermode::try_from_acronyms(s).ok_or_else(|| error!("Invalid mods: {}", s))?;
let mut inner = intermode
.try_with_mode(mode.into())
.ok_or_else(|| error!("Invalid mods for `{}`: {}", mode, intermode))?;
// Always add classic mod to `inner`
inner.insert(Self::classic_mod_of(mode));
if !inner.is_valid() {
return Err(error!("Incompatible mods found: {}", inner));
}
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 {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
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)?;
let is_lazer = !self.inner.contains_intermode(GameModIntermode::Classic);
let mods = if !is_lazer {
let mut v = self.inner.clone();
v.remove_intermode(GameModIntermode::Classic);
Cow::Owned(v)
} else {
Cow::Borrowed(&self.inner)
};
if !mods.is_empty() {
write!(f, "+{}", mods)?;
}
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)?;
}
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::{
self as rosu,
mods::{GameModIntermode, GameModsIntermode},
};
use rosu_v2::model::{self as rosu};
use super::*;
@ -244,180 +241,180 @@ impl From<rosu::Grade> for Rank {
}
}
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);
}
}
if !value.contains(Mods::LAZER) {
res.insert(GameModIntermode::Classic);
}
res
}
}
// 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);
// }
// }
// if !value.contains(Mods::LAZER) {
// res.insert(GameModIntermode::Classic);
// }
// res
// }
// }
impl From<rosu::mods::GameModsIntermode> for Mods {
fn from(value: rosu_v2::prelude::GameModsIntermode) -> Self {
let init = if value.contains(GameModIntermode::Classic) {
Mods::NOMOD
} else {
Mods::LAZER
};
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::DT | Mods::NC,
GameModIntermode::Flashlight => Mods::FL,
GameModIntermode::Autoplay => Mods::AT,
GameModIntermode::SpunOut => Mods::SO,
GameModIntermode::Autopilot => Mods::AP,
GameModIntermode::Perfect => Mods::SD | 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::NOMOD,
_ => Mods::UNKNOWN,
})
.fold(init, |a, b| a | b)
// impl From<rosu::mods::GameModsIntermode> for Mods {
// fn from(value: rosu_v2::prelude::GameModsIntermode) -> Self {
// let init = if value.contains(GameModIntermode::Classic) {
// Mods::NOMOD
// } else {
// Mods::LAZER
// };
// 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::DT | Mods::NC,
// GameModIntermode::Flashlight => Mods::FL,
// GameModIntermode::Autoplay => Mods::AT,
// GameModIntermode::SpunOut => Mods::SO,
// GameModIntermode::Autopilot => Mods::AP,
// GameModIntermode::Perfect => Mods::SD | 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::NOMOD,
// _ => Mods::UNKNOWN,
// })
// .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 {
fn from(value: rosu::mods::GameMods) -> Self {
let unknown =
rosu::mods::GameModIntermode::Unknown(rosu_v2::prelude::UnknownMod::default());
value
.iter()
.cloned()
.map(|m| match m {
rosu::mods::GameMod::HalfTimeOsu(ht)
if ht.speed_change.is_some_and(|v| v != 0.75) =>
{
unknown
}
rosu::mods::GameMod::DaycoreOsu(dc)
if dc.speed_change.is_some_and(|v| v != 0.75) =>
{
unknown
}
rosu::mods::GameMod::DaycoreOsu(_) => rosu::mods::GameModIntermode::HalfTime,
rosu::mods::GameMod::DoubleTimeOsu(dt)
if dt.speed_change.is_some_and(|v| v != 1.5) =>
{
unknown
}
rosu::mods::GameMod::NightcoreOsu(nc)
if nc.speed_change.is_some_and(|v| v != 1.5) =>
{
unknown
}
rosu::mods::GameMod::HalfTimeTaiko(ht)
if ht.speed_change.is_some_and(|v| v != 0.75) =>
{
unknown
}
rosu::mods::GameMod::DaycoreTaiko(dc)
if dc.speed_change.is_some_and(|v| v != 0.75) =>
{
unknown
}
rosu::mods::GameMod::DaycoreTaiko(_) => rosu::mods::GameModIntermode::HalfTime,
rosu::mods::GameMod::DoubleTimeTaiko(dt)
if dt.speed_change.is_some_and(|v| v != 1.5) =>
{
unknown
}
rosu::mods::GameMod::NightcoreTaiko(nc)
if nc.speed_change.is_some_and(|v| v != 1.5) =>
{
unknown
}
rosu::mods::GameMod::HalfTimeCatch(ht)
if ht.speed_change.is_some_and(|v| v != 0.75) =>
{
unknown
}
rosu::mods::GameMod::DaycoreCatch(dc)
if dc.speed_change.is_some_and(|v| v != 0.75) =>
{
unknown
}
rosu::mods::GameMod::DaycoreCatch(_) => rosu::mods::GameModIntermode::HalfTime,
rosu::mods::GameMod::DoubleTimeCatch(dt)
if dt.speed_change.is_some_and(|v| v != 1.5) =>
{
unknown
}
rosu::mods::GameMod::NightcoreCatch(nc)
if nc.speed_change.is_some_and(|v| v != 1.5) =>
{
unknown
}
rosu::mods::GameMod::HalfTimeMania(ht)
if ht.speed_change.is_some_and(|v| v != 0.75) =>
{
unknown
}
rosu::mods::GameMod::DaycoreMania(dc)
if dc.speed_change.is_some_and(|v| v != 0.75) =>
{
unknown
}
rosu::mods::GameMod::DaycoreMania(_) => rosu::mods::GameModIntermode::HalfTime,
rosu::mods::GameMod::DoubleTimeMania(dt)
if dt.speed_change.is_some_and(|v| v != 1.5) =>
{
unknown
}
_ => m.intermode(),
})
.collect::<GameModsIntermode>()
.into()
}
}
// impl From<rosu::mods::GameMods> for Mods {
// fn from(value: rosu::mods::GameMods) -> Self {
// let unknown =
// rosu::mods::GameModIntermode::Unknown(rosu_v2::prelude::UnknownMod::default());
// value
// .iter()
// .cloned()
// .map(|m| match m {
// rosu::mods::GameMod::HalfTimeOsu(ht)
// if ht.speed_change.is_some_and(|v| v != 0.75) =>
// {
// unknown
// }
// rosu::mods::GameMod::DaycoreOsu(dc)
// if dc.speed_change.is_some_and(|v| v != 0.75) =>
// {
// unknown
// }
// rosu::mods::GameMod::DaycoreOsu(_) => rosu::mods::GameModIntermode::HalfTime,
// rosu::mods::GameMod::DoubleTimeOsu(dt)
// if dt.speed_change.is_some_and(|v| v != 1.5) =>
// {
// unknown
// }
// rosu::mods::GameMod::NightcoreOsu(nc)
// if nc.speed_change.is_some_and(|v| v != 1.5) =>
// {
// unknown
// }
// rosu::mods::GameMod::HalfTimeTaiko(ht)
// if ht.speed_change.is_some_and(|v| v != 0.75) =>
// {
// unknown
// }
// rosu::mods::GameMod::DaycoreTaiko(dc)
// if dc.speed_change.is_some_and(|v| v != 0.75) =>
// {
// unknown
// }
// rosu::mods::GameMod::DaycoreTaiko(_) => rosu::mods::GameModIntermode::HalfTime,
// rosu::mods::GameMod::DoubleTimeTaiko(dt)
// if dt.speed_change.is_some_and(|v| v != 1.5) =>
// {
// unknown
// }
// rosu::mods::GameMod::NightcoreTaiko(nc)
// if nc.speed_change.is_some_and(|v| v != 1.5) =>
// {
// unknown
// }
// rosu::mods::GameMod::HalfTimeCatch(ht)
// if ht.speed_change.is_some_and(|v| v != 0.75) =>
// {
// unknown
// }
// rosu::mods::GameMod::DaycoreCatch(dc)
// if dc.speed_change.is_some_and(|v| v != 0.75) =>
// {
// unknown
// }
// rosu::mods::GameMod::DaycoreCatch(_) => rosu::mods::GameModIntermode::HalfTime,
// rosu::mods::GameMod::DoubleTimeCatch(dt)
// if dt.speed_change.is_some_and(|v| v != 1.5) =>
// {
// unknown
// }
// rosu::mods::GameMod::NightcoreCatch(nc)
// if nc.speed_change.is_some_and(|v| v != 1.5) =>
// {
// unknown
// }
// rosu::mods::GameMod::HalfTimeMania(ht)
// if ht.speed_change.is_some_and(|v| v != 0.75) =>
// {
// unknown
// }
// rosu::mods::GameMod::DaycoreMania(dc)
// if dc.speed_change.is_some_and(|v| v != 0.75) =>
// {
// unknown
// }
// rosu::mods::GameMod::DaycoreMania(_) => rosu::mods::GameModIntermode::HalfTime,
// rosu::mods::GameMod::DoubleTimeMania(dt)
// if dt.speed_change.is_some_and(|v| v != 1.5) =>
// {
// unknown
// }
// _ => m.intermode(),
// })
// .collect::<GameModsIntermode>()
// .into()
// }
// }

View file

@ -1,7 +1,7 @@
use core::fmt;
use crate::models::{Mode, Mods};
use crate::Client;
use crate::OsuClient;
use rosu_v2::error::OsuError;
use youmubot_prelude::*;
@ -58,6 +58,7 @@ pub mod builders {
use crate::models;
use super::OsuClient;
use super::*;
/// A builder for a Beatmap request.
pub struct BeatmapRequestBuilder {
@ -82,7 +83,7 @@ pub mod builders {
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 {
BeatmapRequestKind::Beatmap(id) => {
match handle_not_found(client.rosu.beatmap().map_id(id as u32).await)? {
@ -141,7 +142,7 @@ pub mod builders {
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);
if let Some(mode) = self.mode {
r = r.mode(mode.into());
@ -154,7 +155,7 @@ pub mod builders {
- time::Duration::DAY * self.event_days.unwrap_or(31);
let mut events = handle_not_found(client.rosu.recent_activity(user.user_id).await)?
.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();
Ok(Some(models::User::from_rosu(user, stats, events)))
}
@ -199,31 +200,29 @@ pub mod builders {
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 {
Some(user) => {
let mut r = client
.rosu
.beatmap_user_scores(self.beatmap_id as u32, user);
let mut r = osu.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)));
// let mods = GameModsIntermode::from(mods.inner);
ss.retain(|s| Mods::from(s.mods.clone()).contains(&mods));
ss
}),
None => r.await,
}
}
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 {
r = r.mode(mode.into());
}
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 {
r = r.limit(limit as u32);
@ -268,7 +267,7 @@ pub mod builders {
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 mut r = client.rosu.user_scores(self.user);
r = match self.score_type {

View file

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

View file

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