mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-18 00:08:54 +00:00
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:
parent
7565a6e5c5
commit
735b382102
20 changed files with 788 additions and 443 deletions
35
flake.lock
generated
35
flake.lock
generated
|
@ -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": {
|
||||
|
|
16
flake.nix
16
flake.nix
|
@ -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
1
rust-toolchain
Normal file
|
@ -0,0 +1 @@
|
|||
1.79.0
|
|
@ -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;
|
||||
|
|
|
@ -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 }
|
||||
}
|
||||
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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)]),
|
||||
),
|
||||
)
|
||||
|
|
|
@ -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))
|
||||
}
|
||||
|
|
|
@ -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<_>
|
||||
})
|
||||
|
|
|
@ -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)?,
|
||||
|
|
|
@ -446,7 +446,7 @@ pub async fn get_leaderboard(
|
|||
score.count_50,
|
||||
score.count_miss,
|
||||
),
|
||||
score.mods,
|
||||
&score.mods,
|
||||
)
|
||||
.ok()
|
||||
.map(|v| (false, v))
|
||||
|
|
|
@ -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())),
|
||||
})
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
// }
|
||||
// }
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -142,7 +142,6 @@ impl AnnouncerHandler {
|
|||
}
|
||||
|
||||
pub fn run(self, client: &Client) -> AnnouncerRunner {
|
||||
|
||||
AnnouncerRunner {
|
||||
cache_http: CacheAndHttp::from_client(client),
|
||||
data: client.data.clone(),
|
||||
|
|
|
@ -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,
|
||||
|
|
Loading…
Add table
Reference in a new issue