mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 16:58:55 +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": {
|
"locked": {
|
||||||
"lastModified": 1718730147,
|
"lastModified": 1724377159,
|
||||||
"narHash": "sha256-QmD6B6FYpuoCqu6ZuPJH896ItNquDkn0ulQlOn4ykN8=",
|
"narHash": "sha256-ixjje1JO8ucKT41hs6n2NCde1Vc0+Zc2p2gUbJpCsMw=",
|
||||||
"owner": "ipetkov",
|
"owner": "ipetkov",
|
||||||
"repo": "crane",
|
"repo": "crane",
|
||||||
"rev": "32c21c29b034d0a93fdb2379d6fabc40fc3d0e6c",
|
"rev": "3e47b7a86c19142bd3675da49d6acef488b4dac1",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -40,11 +40,11 @@
|
||||||
},
|
},
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1718530797,
|
"lastModified": 1724224976,
|
||||||
"narHash": "sha256-pup6cYwtgvzDpvpSCFh1TEUjw2zkNpk8iolbKnyFmmU=",
|
"narHash": "sha256-Z/ELQhrSd7bMzTO8r7NZgi9g5emh+aRKoCdaAv5fiO0=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "b60ebf54c15553b393d144357375ea956f89e9a9",
|
"rev": "c374d94f1536013ca8e92341b540eba4c22f9c62",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
|
@ -58,7 +58,28 @@
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"crane": "crane",
|
"crane": "crane",
|
||||||
"flake-utils": "flake-utils",
|
"flake-utils": "flake-utils",
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs",
|
||||||
|
"rust-overlay": "rust-overlay"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"rust-overlay": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": [
|
||||||
|
"nixpkgs"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1724466314,
|
||||||
|
"narHash": "sha256-ltKuK6shQ64uej1mYNtBsDYxttUNFiv9AcHqk0+0NQM=",
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"rev": "2b5b3edd96ef336b00622dcabc13788fdef9e3ca",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "oxalica",
|
||||||
|
"repo": "rust-overlay",
|
||||||
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"systems": {
|
"systems": {
|
||||||
|
|
16
flake.nix
16
flake.nix
|
@ -7,16 +7,24 @@
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
};
|
};
|
||||||
flake-utils.url = "github:numtide/flake-utils";
|
flake-utils.url = "github:numtide/flake-utils";
|
||||||
|
rust-overlay = {
|
||||||
|
url = "github:oxalica/rust-overlay";
|
||||||
|
inputs.nixpkgs.follows = "nixpkgs";
|
||||||
|
};
|
||||||
};
|
};
|
||||||
nixConfig = {
|
nixConfig = {
|
||||||
extra-substituters = [ "https://natsukagami.cachix.org" ];
|
extra-substituters = [ "https://natsukagami.cachix.org" ];
|
||||||
trusted-public-keys = [ "natsukagami.cachix.org-1:3U6GV8i8gWEaXRUuXd2S4ASfYgdl2QFPWg4BKPbmYiQ=" ];
|
trusted-public-keys = [ "cache.nixos.org-1:6NCHdD59X431o0gWypbMrAURkbJ16ZPMQFGspcDShjY=" "natsukagami.cachix.org-1:3U6GV8i8gWEaXRUuXd2S4ASfYgdl2QFPWg4BKPbmYiQ=" ];
|
||||||
};
|
};
|
||||||
outputs = { self, nixpkgs, flake-utils, ... }@inputs: flake-utils.lib.eachDefaultSystem
|
outputs = { self, nixpkgs, flake-utils, ... }@inputs: flake-utils.lib.eachDefaultSystem
|
||||||
(system:
|
(system:
|
||||||
let
|
let
|
||||||
pkgs = import nixpkgs { inherit system; };
|
pkgs = import nixpkgs
|
||||||
craneLib = inputs.crane.mkLib pkgs;
|
{
|
||||||
|
inherit system; overlays = [ (import inputs.rust-overlay) ];
|
||||||
|
};
|
||||||
|
craneLib = (inputs.crane.mkLib pkgs).overrideToolchain (p: p.rust-bin.stable."1.79.0".default);
|
||||||
|
# craneLib = inputs.crane.mkLib pkgs;
|
||||||
in
|
in
|
||||||
rec {
|
rec {
|
||||||
packages.youmubot = pkgs.callPackage ./package.nix { inherit craneLib; };
|
packages.youmubot = pkgs.callPackage ./package.nix { inherit craneLib; };
|
||||||
|
@ -35,7 +43,7 @@
|
||||||
{
|
{
|
||||||
inputsFrom = [ packages.youmubot ];
|
inputsFrom = [ packages.youmubot ];
|
||||||
|
|
||||||
buildInputs = with pkgs; [ rustc rustfmt clippy sqlx-cli rust-analyzer ];
|
buildInputs = with pkgs; [ rustfmt clippy sqlx-cli rust-analyzer ];
|
||||||
|
|
||||||
nativeBuildInputs = nixpkgs.lib.optionals pkgs.stdenv.isLinux (with pkgs; [
|
nativeBuildInputs = nixpkgs.lib.optionals pkgs.stdenv.isLinux (with pkgs; [
|
||||||
pkg-config
|
pkg-config
|
||||||
|
|
1
rust-toolchain
Normal file
1
rust-toolchain
Normal file
|
@ -0,0 +1 @@
|
||||||
|
1.79.0
|
|
@ -27,7 +27,7 @@ use crate::{
|
||||||
discord::oppai_cache::BeatmapContent,
|
discord::oppai_cache::BeatmapContent,
|
||||||
models::{Mode, Score, User, UserEventRank},
|
models::{Mode, Score, User, UserEventRank},
|
||||||
request::UserID,
|
request::UserID,
|
||||||
Client as Osu,
|
OsuClient as Osu,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::db::OsuUser;
|
use super::db::OsuUser;
|
||||||
|
|
|
@ -5,14 +5,14 @@ use youmubot_prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
models::{ApprovalStatus, Beatmap, Mode},
|
models::{ApprovalStatus, Beatmap, Mode},
|
||||||
Client,
|
OsuClient,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling.
|
/// BeatmapMetaCache intercepts beatmap-by-id requests and caches them for later recalling.
|
||||||
/// Does not cache non-Ranked beatmaps.
|
/// Does not cache non-Ranked beatmaps.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct BeatmapMetaCache {
|
pub struct BeatmapMetaCache {
|
||||||
client: Arc<Client>,
|
client: Arc<OsuClient>,
|
||||||
pool: Pool,
|
pool: Pool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -28,7 +28,7 @@ impl TypeMapKey for BeatmapMetaCache {
|
||||||
|
|
||||||
impl BeatmapMetaCache {
|
impl BeatmapMetaCache {
|
||||||
/// Create a new beatmap cache.
|
/// Create a new beatmap cache.
|
||||||
pub fn new(client: Arc<Client>, pool: Pool) -> Self {
|
pub fn new(client: Arc<OsuClient>, pool: Pool) -> Self {
|
||||||
BeatmapMetaCache { client, pool }
|
BeatmapMetaCache { client, pool }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -211,7 +211,7 @@ mod scores {
|
||||||
let beatmap = meta_cache.get_beatmap(play.beatmap_id, mode).await?;
|
let beatmap = meta_cache.get_beatmap(play.beatmap_id, mode).await?;
|
||||||
let info = {
|
let info = {
|
||||||
let b = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
let b = oppai.get_beatmap(beatmap.beatmap_id).await?;
|
||||||
b.get_info_with(mode, play.mods).ok()
|
b.get_info_with(mode, &play.mods).ok()
|
||||||
};
|
};
|
||||||
Ok((beatmap, info)) as Result<(Beatmap, Option<BeatmapInfo>)>
|
Ok((beatmap, info)) as Result<(Beatmap, Option<BeatmapInfo>)>
|
||||||
})
|
})
|
||||||
|
@ -236,7 +236,7 @@ mod scores {
|
||||||
p.count_50,
|
p.count_50,
|
||||||
p.count_miss,
|
p.count_miss,
|
||||||
),
|
),
|
||||||
p.mods,
|
&p.mods,
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
.map(|pp| format!("{:.2}[?]", pp))
|
.map(|pp| format!("{:.2}[?]", pp))
|
||||||
|
@ -291,7 +291,7 @@ mod scores {
|
||||||
beatmap.artist,
|
beatmap.artist,
|
||||||
beatmap.title,
|
beatmap.title,
|
||||||
beatmap.difficulty_name,
|
beatmap.difficulty_name,
|
||||||
beatmap.short_link(Some(self.mode), Some(play.mods)),
|
beatmap.short_link(Some(self.mode), &play.mods),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| "FETCH_FAILED".to_owned())
|
.unwrap_or_else(|| "FETCH_FAILED".to_owned())
|
||||||
|
@ -359,13 +359,11 @@ mod beatmapset {
|
||||||
ctx: &Context,
|
ctx: &Context,
|
||||||
beatmapset: Vec<Beatmap>,
|
beatmapset: Vec<Beatmap>,
|
||||||
mode: Option<Mode>,
|
mode: Option<Mode>,
|
||||||
mods: Option<Mods>,
|
mods: Mods,
|
||||||
reply_to: &Message,
|
reply_to: &Message,
|
||||||
guild_id: Option<GuildId>,
|
guild_id: Option<GuildId>,
|
||||||
message: impl AsRef<str>,
|
message: impl AsRef<str>,
|
||||||
) -> Result<bool> {
|
) -> Result<bool> {
|
||||||
let mods = mods.unwrap_or(Mods::NOMOD);
|
|
||||||
|
|
||||||
if beatmapset.is_empty() {
|
if beatmapset.is_empty() {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
@ -405,7 +403,7 @@ mod beatmapset {
|
||||||
env.oppai
|
env.oppai
|
||||||
.get_beatmap(b.beatmap_id)
|
.get_beatmap(b.beatmap_id)
|
||||||
.await
|
.await
|
||||||
.and_then(move |v| v.get_possible_pp_with(self.mode.unwrap_or(b.mode), self.mods))
|
.and_then(move |v| v.get_possible_pp_with(self.mode.unwrap_or(b.mode), &self.mods))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -446,7 +444,7 @@ mod beatmapset {
|
||||||
crate::discord::embeds::beatmap_embed(
|
crate::discord::embeds::beatmap_embed(
|
||||||
map,
|
map,
|
||||||
self.mode.unwrap_or(map.mode),
|
self.mode.unwrap_or(map.mode),
|
||||||
self.mods,
|
&self.mods,
|
||||||
info,
|
info,
|
||||||
)
|
)
|
||||||
.footer({
|
.footer({
|
||||||
|
|
|
@ -25,7 +25,7 @@ pub(crate) fn grouped_number(num: u64) -> String {
|
||||||
b.build()
|
b.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn beatmap_description(b: &Beatmap) -> String {
|
fn beatmap_description(b: &Beatmap, mods: &Mods) -> String {
|
||||||
MessageBuilder::new()
|
MessageBuilder::new()
|
||||||
.push_bold_line(b.approval.to_string())
|
.push_bold_line(b.approval.to_string())
|
||||||
.push({
|
.push({
|
||||||
|
@ -59,13 +59,24 @@ fn beatmap_description(b: &Beatmap) -> String {
|
||||||
.collect::<Vec<_>>()
|
.collect::<Vec<_>>()
|
||||||
.join(" "),
|
.join(" "),
|
||||||
)
|
)
|
||||||
|
.push_line(mod_details(mods).unwrap_or("".into()))
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn mod_details(mods: &Mods) -> Option<Cow<'_, str>> {
|
||||||
|
let mut d = mods.details();
|
||||||
|
if d.is_empty() {
|
||||||
|
None
|
||||||
|
} else {
|
||||||
|
d.insert(0, "**Mods**:".to_owned());
|
||||||
|
Some(Cow::from(d.join("\n- ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn beatmap_offline_embed(
|
pub fn beatmap_offline_embed(
|
||||||
b: &'_ crate::discord::oppai_cache::BeatmapContent,
|
b: &'_ crate::discord::oppai_cache::BeatmapContent,
|
||||||
m: Mode,
|
m: Mode,
|
||||||
mods: Mods,
|
mods: &Mods,
|
||||||
) -> Result<(CreateEmbed, Vec<CreateAttachment>)> {
|
) -> Result<(CreateEmbed, Vec<CreateAttachment>)> {
|
||||||
let bm = b.content.clone();
|
let bm = b.content.clone();
|
||||||
let metadata = b.metadata.clone();
|
let metadata = b.metadata.clone();
|
||||||
|
@ -150,7 +161,7 @@ fn beatmap_title(
|
||||||
artist: impl AsRef<str>,
|
artist: impl AsRef<str>,
|
||||||
title: impl AsRef<str>,
|
title: impl AsRef<str>,
|
||||||
difficulty: impl AsRef<str>,
|
difficulty: impl AsRef<str>,
|
||||||
mods: Mods,
|
mods: &Mods,
|
||||||
) -> String {
|
) -> String {
|
||||||
let mod_str = if mods == Mods::NOMOD {
|
let mod_str = if mods == Mods::NOMOD {
|
||||||
"".to_owned()
|
"".to_owned()
|
||||||
|
@ -168,7 +179,7 @@ fn beatmap_title(
|
||||||
.build()
|
.build()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn beatmap_embed(b: &'_ Beatmap, m: Mode, mods: Mods, info: BeatmapInfoWithPP) -> CreateEmbed {
|
pub fn beatmap_embed(b: &'_ Beatmap, m: Mode, mods: &Mods, info: BeatmapInfoWithPP) -> CreateEmbed {
|
||||||
let diff = b.difficulty.apply_mods(mods, info.0.stars);
|
let diff = b.difficulty.apply_mods(mods, info.0.stars);
|
||||||
CreateEmbed::new()
|
CreateEmbed::new()
|
||||||
.title(beatmap_title(&b.artist, &b.title, &b.difficulty_name, mods))
|
.title(beatmap_title(&b.artist, &b.title, &b.difficulty_name, mods))
|
||||||
|
@ -192,7 +203,7 @@ pub fn beatmap_embed(b: &'_ Beatmap, m: Mode, mods: Mods, info: BeatmapInfoWithP
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
.field("Information", diff.format_info(m, mods, b), false)
|
.field("Information", diff.format_info(m, mods, b), false)
|
||||||
.description(beatmap_description(b))
|
.description(beatmap_description(b, mods))
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_DIFFS: usize = 25 - 4;
|
const MAX_DIFFS: usize = 25 - 4;
|
||||||
|
@ -222,7 +233,7 @@ pub fn beatmapset_embed(bs: &'_ [Beatmap], m: Option<Mode>) -> CreateEmbed {
|
||||||
b.beatmapset_id
|
b.beatmapset_id
|
||||||
))
|
))
|
||||||
.color(0xffb6c1)
|
.color(0xffb6c1)
|
||||||
.description(beatmap_description(b))
|
.description(beatmap_description(b, Mods::NOMOD))
|
||||||
.fields(bs.iter().rev().take(MAX_DIFFS).rev().map(|b: &Beatmap| {
|
.fields(bs.iter().rev().take(MAX_DIFFS).rev().map(|b: &Beatmap| {
|
||||||
(
|
(
|
||||||
format!("[{}]", b.difficulty_name),
|
format!("[{}]", b.difficulty_name),
|
||||||
|
@ -292,7 +303,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
||||||
let content = self.content;
|
let content = self.content;
|
||||||
let u = &self.u;
|
let u = &self.u;
|
||||||
let accuracy = s.accuracy(mode);
|
let accuracy = s.accuracy(mode);
|
||||||
let info = content.get_info_with(mode, s.mods).ok();
|
let info = content.get_info_with(mode, &s.mods).ok();
|
||||||
let stars = info
|
let stars = info
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map(|info| info.stars)
|
.map(|info| info.stars)
|
||||||
|
@ -322,7 +333,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
||||||
mode,
|
mode,
|
||||||
Some(s.max_combo as usize),
|
Some(s.max_combo as usize),
|
||||||
Accuracy::ByCount(s.count_300, s.count_100, s.count_50, s.count_miss),
|
Accuracy::ByCount(s.count_300, s.count_100, s.count_50, s.count_miss),
|
||||||
s.mods,
|
&s.mods,
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
.map(|pp| (pp, format!("{:.2}pp [?]", pp)))
|
.map(|pp| (pp, format!("{:.2}pp [?]", pp)))
|
||||||
|
@ -333,7 +344,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
||||||
mode,
|
mode,
|
||||||
None,
|
None,
|
||||||
Accuracy::ByCount(s.count_300 + s.count_miss, s.count_100, s.count_50, 0),
|
Accuracy::ByCount(s.count_300 + s.count_miss, s.count_100, s.count_50, 0),
|
||||||
s.mods,
|
&s.mods,
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|&v| pp.as_ref().map(|&(origin, _)| origin < v).unwrap_or(false))
|
.filter(|&v| pp.as_ref().map(|&(origin, _)| origin < v).unwrap_or(false))
|
||||||
|
@ -377,12 +388,34 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
||||||
.or(s.global_rank)
|
.or(s.global_rank)
|
||||||
.map(|v| format!(" | #{} on Global Rankings!", v))
|
.map(|v| format!(" | #{} on Global Rankings!", v))
|
||||||
.unwrap_or_else(|| "".to_owned());
|
.unwrap_or_else(|| "".to_owned());
|
||||||
let diff = b.difficulty.apply_mods(s.mods, stars);
|
let diff = b.difficulty.apply_mods(&s.mods, stars);
|
||||||
let creator = if b.difficulty_name.contains("'s") {
|
let creator = if b.difficulty_name.contains("'s") {
|
||||||
"".to_owned()
|
"".to_owned()
|
||||||
} else {
|
} else {
|
||||||
format!("by {} ", b.creator)
|
format!("by {} ", b.creator)
|
||||||
};
|
};
|
||||||
|
let mod_details = mod_details(&s.mods);
|
||||||
|
let description_fields = [
|
||||||
|
Some(
|
||||||
|
format!(
|
||||||
|
"**Played**: {} {} {}",
|
||||||
|
s.date.format("<t:%s:R>"),
|
||||||
|
s.link()
|
||||||
|
.map(|s| format!("[[Score]]({})", s).into())
|
||||||
|
.unwrap_or(Cow::from("")),
|
||||||
|
s.replay_download_link()
|
||||||
|
.map(|s| format!("[[Replay]]({})", s).into())
|
||||||
|
.unwrap_or(Cow::from("")),
|
||||||
|
)
|
||||||
|
.into(),
|
||||||
|
),
|
||||||
|
pp_gained.as_ref().map(|v| (&v[..]).into()),
|
||||||
|
mod_details,
|
||||||
|
]
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join("\n");
|
||||||
let mut m = CreateEmbed::new()
|
let mut m = CreateEmbed::new()
|
||||||
.author(
|
.author(
|
||||||
CreateEmbedAuthor::new(&u.username)
|
CreateEmbedAuthor::new(&u.username)
|
||||||
|
@ -411,18 +444,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
||||||
.push(world_record)
|
.push(world_record)
|
||||||
.build(),
|
.build(),
|
||||||
)
|
)
|
||||||
.description(format!(
|
.description(description_fields)
|
||||||
r#"**Played**: {} {} {}
|
|
||||||
{}"#,
|
|
||||||
s.date.format("<t:%s:R>"),
|
|
||||||
s.link()
|
|
||||||
.map(|s| format!("[[Score]]({})", s).into())
|
|
||||||
.unwrap_or(Cow::from("")),
|
|
||||||
s.replay_download_link()
|
|
||||||
.map(|s| format!("[[Replay]]({})", s).into())
|
|
||||||
.unwrap_or(Cow::from("")),
|
|
||||||
pp_gained.as_ref().map(|v| &v[..]).unwrap_or(""),
|
|
||||||
))
|
|
||||||
.thumbnail(b.thumbnail_url())
|
.thumbnail(b.thumbnail_url())
|
||||||
.field(
|
.field(
|
||||||
"Score stats",
|
"Score stats",
|
||||||
|
@ -442,9 +464,9 @@ impl<'a> ScoreEmbedBuilder<'a> {
|
||||||
),
|
),
|
||||||
true,
|
true,
|
||||||
)
|
)
|
||||||
.field("Map stats", diff.format_info(mode, s.mods, b), false);
|
.field("Map stats", diff.format_info(mode, &s.mods, b), false);
|
||||||
let mut footer = self.footer.take().unwrap_or_default();
|
let mut footer = self.footer.take().unwrap_or_default();
|
||||||
if mode != Mode::Std && s.mods != Mods::NOMOD {
|
if mode != Mode::Std && &s.mods != Mods::NOMOD {
|
||||||
footer += " Star difficulty does not reflect game mods.";
|
footer += " Star difficulty does not reflect game mods.";
|
||||||
}
|
}
|
||||||
if !footer.is_empty() {
|
if !footer.is_empty() {
|
||||||
|
@ -557,8 +579,8 @@ pub(crate) fn user_embed(
|
||||||
.push(format!(
|
.push(format!(
|
||||||
"> {}",
|
"> {}",
|
||||||
map.difficulty
|
map.difficulty
|
||||||
.apply_mods(v.mods, info.stars)
|
.apply_mods(&v.mods, info.stars)
|
||||||
.format_info(mode, v.mods, &map)
|
.format_info(mode, &v.mods, &map)
|
||||||
.replace('\n', "\n> ")
|
.replace('\n', "\n> ")
|
||||||
))
|
))
|
||||||
.build(),
|
.build(),
|
||||||
|
|
|
@ -123,10 +123,11 @@ pub fn dot_osu_hook<'a>(
|
||||||
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
let env = ctx.data.read().await.get::<OsuEnv>().unwrap().clone();
|
||||||
|
|
||||||
let (beatmap, _) = env.oppai.download_beatmap_from_url(&url).await.ok()?;
|
let (beatmap, _) = env.oppai.download_beatmap_from_url(&url).await.ok()?;
|
||||||
|
let m = Mode::from(beatmap.content.mode as u8);
|
||||||
crate::discord::embeds::beatmap_offline_embed(
|
crate::discord::embeds::beatmap_offline_embed(
|
||||||
&beatmap,
|
&beatmap,
|
||||||
Mode::from(beatmap.content.mode as u8), /*For now*/
|
m, /*For now*/
|
||||||
msg.content.trim().parse().unwrap_or(Mods::NOMOD),
|
&Mods::from_str(msg.content.trim(), m).unwrap_or_default(),
|
||||||
)
|
)
|
||||||
.pls_ok()
|
.pls_ok()
|
||||||
}
|
}
|
||||||
|
@ -152,10 +153,11 @@ pub fn dot_osu_hook<'a>(
|
||||||
beatmaps
|
beatmaps
|
||||||
.into_iter()
|
.into_iter()
|
||||||
.filter_map(|beatmap| {
|
.filter_map(|beatmap| {
|
||||||
|
let m = Mode::from(beatmap.content.mode as u8);
|
||||||
crate::discord::embeds::beatmap_offline_embed(
|
crate::discord::embeds::beatmap_offline_embed(
|
||||||
&beatmap,
|
&beatmap,
|
||||||
Mode::from(beatmap.content.mode as u8), /*For now*/
|
m, /*For now*/
|
||||||
msg.content.trim().parse().unwrap_or(Mods::NOMOD),
|
&Mods::from_str(msg.content.trim(), m).unwrap_or_default(),
|
||||||
)
|
)
|
||||||
.pls_ok()
|
.pls_ok()
|
||||||
})
|
})
|
||||||
|
@ -300,7 +302,7 @@ async fn handle_beatmap<'a, 'b>(
|
||||||
.embed(beatmap_embed(
|
.embed(beatmap_embed(
|
||||||
beatmap,
|
beatmap,
|
||||||
mode.unwrap_or(beatmap.mode),
|
mode.unwrap_or(beatmap.mode),
|
||||||
mods,
|
&mods,
|
||||||
info,
|
info,
|
||||||
))
|
))
|
||||||
.components(vec![beatmap_components(reply_to.guild_id)])
|
.components(vec![beatmap_components(reply_to.guild_id)])
|
||||||
|
@ -321,7 +323,7 @@ async fn handle_beatmapset<'a, 'b>(
|
||||||
ctx,
|
ctx,
|
||||||
beatmaps,
|
beatmaps,
|
||||||
mode,
|
mode,
|
||||||
None,
|
Mods::default(),
|
||||||
reply_to,
|
reply_to,
|
||||||
reply_to.guild_id,
|
reply_to.guild_id,
|
||||||
format!("Beatmapset information for `{}`", link),
|
format!("Beatmapset information for `{}`", link),
|
||||||
|
|
|
@ -147,18 +147,21 @@ pub fn handle_last_button<'a>(
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let BeatmapWithMode(b, m) = &bm;
|
let BeatmapWithMode(b, m) = &bm;
|
||||||
|
|
||||||
let mods = mods_def.unwrap_or(Mods::NOMOD);
|
let mods = mods_def.unwrap_or_default();
|
||||||
let info = env
|
let info = env
|
||||||
.oppai
|
.oppai
|
||||||
.get_beatmap(b.beatmap_id)
|
.get_beatmap(b.beatmap_id)
|
||||||
.await?
|
.await?
|
||||||
.get_possible_pp_with(*m, mods)?;
|
.get_possible_pp_with(*m, &mods)?;
|
||||||
comp.create_response(
|
comp.create_response(
|
||||||
&ctx,
|
&ctx,
|
||||||
serenity::all::CreateInteractionResponse::Message(
|
serenity::all::CreateInteractionResponse::Message(
|
||||||
CreateInteractionResponseMessage::new()
|
CreateInteractionResponseMessage::new()
|
||||||
.content(format!("Information for beatmap `{}`", bm.short_link(mods)))
|
.content(format!(
|
||||||
.embed(beatmap_embed(b, *m, mods, info))
|
"Information for beatmap `{}`",
|
||||||
|
bm.short_link(&mods)
|
||||||
|
))
|
||||||
|
.embed(beatmap_embed(b, *m, &mods, info))
|
||||||
.components(vec![beatmap_components(comp.guild_id)]),
|
.components(vec![beatmap_components(comp.guild_id)]),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
|
@ -2,6 +2,7 @@ use std::str::FromStr;
|
||||||
|
|
||||||
use crate::models::*;
|
use crate::models::*;
|
||||||
use lazy_static::lazy_static;
|
use lazy_static::lazy_static;
|
||||||
|
use mods::UnparsedMods;
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use stream::Stream;
|
use stream::Stream;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
@ -22,7 +23,7 @@ pub struct ToPrint<'a> {
|
||||||
lazy_static! {
|
lazy_static! {
|
||||||
// Beatmap(set) hooks
|
// Beatmap(set) hooks
|
||||||
static ref OLD_LINK_REGEX: Regex = Regex::new(
|
static ref OLD_LINK_REGEX: Regex = Regex::new(
|
||||||
r"(?:https?://)?osu\.ppy\.sh/(?P<link_type>s|b)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>[0123]))?(?:\+(?P<mods>[A-Z]+))?"
|
r"(?:https?://)?osu\.ppy\.sh/(?P<link_type>s|b|beatmaps)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>[0123]))?(?:\+(?P<mods>[A-Z]+))?"
|
||||||
).unwrap();
|
).unwrap();
|
||||||
static ref NEW_LINK_REGEX: Regex = Regex::new(
|
static ref NEW_LINK_REGEX: Regex = Regex::new(
|
||||||
r"(?:https?://)?osu\.ppy\.sh/beatmapsets/(?P<set_id>\d+)/?(?:\#(?P<mode>osu|taiko|fruits|mania)(?:/(?P<beatmap_id>\d+)|/?))?(?:\+(?P<mods>[A-Z]+))?"
|
r"(?:https?://)?osu\.ppy\.sh/beatmapsets/(?P<set_id>\d+)/?(?:\#(?P<mode>osu|taiko|fruits|mania)(?:/(?P<beatmap_id>\d+)|/?))?(?:\+(?P<mods>[A-Z]+))?"
|
||||||
|
@ -51,12 +52,12 @@ pub fn parse_old_links<'a>(
|
||||||
.transpose()?
|
.transpose()?
|
||||||
.map(Mode::from);
|
.map(Mode::from);
|
||||||
let embed = match req_type {
|
let embed = match req_type {
|
||||||
"b" => {
|
"b" | "beatmaps" => {
|
||||||
// collect beatmap info
|
// collect beatmap info
|
||||||
let mods = capture
|
let mods = capture
|
||||||
.name("mods")
|
.name("mods")
|
||||||
.and_then(|v| Mods::from_str(v.as_str()).pls_ok())
|
.and_then(|v| UnparsedMods::from_str(v.as_str()).pls_ok())
|
||||||
.unwrap_or(Mods::NOMOD);
|
.unwrap_or_default();
|
||||||
EmbedType::from_beatmap_id(env, capture["id"].parse()?, mode, mods).await
|
EmbedType::from_beatmap_id(env, capture["id"].parse()?, mode, mods).await
|
||||||
}
|
}
|
||||||
"s" => EmbedType::from_beatmapset_id(env, capture["id"].parse()?).await,
|
"s" => EmbedType::from_beatmapset_id(env, capture["id"].parse()?).await,
|
||||||
|
@ -90,8 +91,8 @@ pub fn parse_new_links<'a>(
|
||||||
Some(beatmap_id) => {
|
Some(beatmap_id) => {
|
||||||
let mods = capture
|
let mods = capture
|
||||||
.name("mods")
|
.name("mods")
|
||||||
.and_then(|v| Mods::from_str(v.as_str()).pls_ok())
|
.and_then(|v| UnparsedMods::from_str(v.as_str()).pls_ok())
|
||||||
.unwrap_or(Mods::NOMOD);
|
.unwrap_or_default();
|
||||||
EmbedType::from_beatmap_id(env, beatmap_id, mode, mods).await
|
EmbedType::from_beatmap_id(env, beatmap_id, mode, mods).await
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
@ -122,8 +123,8 @@ pub fn parse_short_links<'a>(
|
||||||
let id: u64 = capture.name("id").unwrap().as_str().parse()?;
|
let id: u64 = capture.name("id").unwrap().as_str().parse()?;
|
||||||
let mods = capture
|
let mods = capture
|
||||||
.name("mods")
|
.name("mods")
|
||||||
.and_then(|v| Mods::from_str(v.as_str()).pls_ok())
|
.and_then(|v| UnparsedMods::from_str(v.as_str()).pls_ok())
|
||||||
.unwrap_or(Mods::NOMOD);
|
.unwrap_or_default();
|
||||||
let embed = EmbedType::from_beatmap_id(env, id, mode, mods).await?;
|
let embed = EmbedType::from_beatmap_id(env, id, mode, mods).await?;
|
||||||
Ok(ToPrint { embed, link, mode })
|
Ok(ToPrint { embed, link, mode })
|
||||||
})
|
})
|
||||||
|
@ -136,18 +137,19 @@ impl EmbedType {
|
||||||
env: &OsuEnv,
|
env: &OsuEnv,
|
||||||
beatmap_id: u64,
|
beatmap_id: u64,
|
||||||
mode: Option<Mode>,
|
mode: Option<Mode>,
|
||||||
mods: Mods,
|
mods: UnparsedMods,
|
||||||
) -> Result<Self> {
|
) -> Result<Self> {
|
||||||
let bm = match mode {
|
let bm = match mode {
|
||||||
Some(mode) => env.beatmaps.get_beatmap(beatmap_id, mode).await?,
|
Some(mode) => env.beatmaps.get_beatmap(beatmap_id, mode).await?,
|
||||||
None => env.beatmaps.get_beatmap_default(beatmap_id).await?,
|
None => env.beatmaps.get_beatmap_default(beatmap_id).await?,
|
||||||
};
|
};
|
||||||
|
let mods = mods.to_mods(mode.unwrap_or(bm.mode))?;
|
||||||
let info = {
|
let info = {
|
||||||
let mode = mode.unwrap_or(bm.mode);
|
let mode = mode.unwrap_or(bm.mode);
|
||||||
env.oppai
|
env.oppai
|
||||||
.get_beatmap(bm.beatmap_id)
|
.get_beatmap(bm.beatmap_id)
|
||||||
.await
|
.await
|
||||||
.and_then(|b| b.get_possible_pp_with(mode, mods))?
|
.and_then(|b| b.get_possible_pp_with(mode, &mods))?
|
||||||
};
|
};
|
||||||
Ok(Self::Beatmap(Box::new(bm), info, mods))
|
Ok(Self::Beatmap(Box::new(bm), info, mods))
|
||||||
}
|
}
|
||||||
|
|
|
@ -25,12 +25,15 @@ use youmubot_prelude::announcer::AnnouncerHandler;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
discord::beatmap_cache::BeatmapMetaCache,
|
discord::{
|
||||||
discord::display::ScoreListStyle,
|
beatmap_cache::BeatmapMetaCache,
|
||||||
discord::oppai_cache::{BeatmapCache, BeatmapInfo},
|
display::ScoreListStyle,
|
||||||
|
oppai_cache::{BeatmapCache, BeatmapInfo},
|
||||||
|
},
|
||||||
models::{Beatmap, Mode, Mods, Score, User},
|
models::{Beatmap, Mode, Mods, Score, User},
|
||||||
|
mods::UnparsedMods,
|
||||||
request::{BeatmapRequestKind, UserID},
|
request::{BeatmapRequestKind, UserID},
|
||||||
Client as OsuHttpClient,
|
OsuClient as OsuHttpClient,
|
||||||
};
|
};
|
||||||
|
|
||||||
mod announcer;
|
mod announcer;
|
||||||
|
@ -49,7 +52,7 @@ mod server_rank;
|
||||||
pub(crate) struct OsuClient;
|
pub(crate) struct OsuClient;
|
||||||
|
|
||||||
impl TypeMapKey for OsuClient {
|
impl TypeMapKey for OsuClient {
|
||||||
type Value = Arc<crate::Client>;
|
type Value = Arc<crate::OsuClient>;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The environment for osu! app commands.
|
/// The environment for osu! app commands.
|
||||||
|
@ -60,7 +63,7 @@ pub struct OsuEnv {
|
||||||
pub(crate) saved_users: OsuSavedUsers,
|
pub(crate) saved_users: OsuSavedUsers,
|
||||||
pub(crate) last_beatmaps: OsuLastBeatmap,
|
pub(crate) last_beatmaps: OsuLastBeatmap,
|
||||||
// clients
|
// clients
|
||||||
pub(crate) client: Arc<crate::Client>,
|
pub(crate) client: Arc<crate::OsuClient>,
|
||||||
pub(crate) oppai: BeatmapCache,
|
pub(crate) oppai: BeatmapCache,
|
||||||
pub(crate) beatmaps: BeatmapMetaCache,
|
pub(crate) beatmaps: BeatmapMetaCache,
|
||||||
}
|
}
|
||||||
|
@ -199,8 +202,8 @@ pub async fn mania(ctx: &Context, msg: &Message, args: Args) -> CommandResult {
|
||||||
pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode);
|
pub(crate) struct BeatmapWithMode(pub Beatmap, pub Mode);
|
||||||
|
|
||||||
impl BeatmapWithMode {
|
impl BeatmapWithMode {
|
||||||
pub fn short_link(&self, mods: Mods) -> String {
|
pub fn short_link(&self, mods: &Mods) -> String {
|
||||||
self.0.short_link(Some(self.1), Some(mods))
|
self.0.short_link(Some(self.1), mods)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mode(&self) -> Mode {
|
fn mode(&self) -> Mode {
|
||||||
|
@ -623,14 +626,17 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
|
|
||||||
match b {
|
match b {
|
||||||
Some((bm, mods_def)) => {
|
Some((bm, mods_def)) => {
|
||||||
let mods = args.find::<Mods>().ok().or(mods_def).unwrap_or(Mods::NOMOD);
|
let mods = match args.find::<UnparsedMods>().ok() {
|
||||||
|
Some(m) => m.to_mods(bm.mode())?,
|
||||||
|
None => mods_def.unwrap_or_default(),
|
||||||
|
};
|
||||||
if beatmapset {
|
if beatmapset {
|
||||||
let beatmapset = env.beatmaps.get_beatmapset(bm.0.beatmapset_id).await?;
|
let beatmapset = env.beatmaps.get_beatmapset(bm.0.beatmapset_id).await?;
|
||||||
display::display_beatmapset(
|
display::display_beatmapset(
|
||||||
ctx,
|
ctx,
|
||||||
beatmapset,
|
beatmapset,
|
||||||
None,
|
None,
|
||||||
Some(mods),
|
mods,
|
||||||
msg,
|
msg,
|
||||||
msg.guild_id,
|
msg.guild_id,
|
||||||
"Here is the beatmapset you requested!",
|
"Here is the beatmapset you requested!",
|
||||||
|
@ -642,13 +648,13 @@ pub async fn last(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
|
||||||
.oppai
|
.oppai
|
||||||
.get_beatmap(bm.0.beatmap_id)
|
.get_beatmap(bm.0.beatmap_id)
|
||||||
.await?
|
.await?
|
||||||
.get_possible_pp_with(bm.1, mods)?;
|
.get_possible_pp_with(bm.1, &mods)?;
|
||||||
msg.channel_id
|
msg.channel_id
|
||||||
.send_message(
|
.send_message(
|
||||||
&ctx,
|
&ctx,
|
||||||
CreateMessage::new()
|
CreateMessage::new()
|
||||||
.content("Here is the beatmap you requested!")
|
.content("Here is the beatmap you requested!")
|
||||||
.embed(beatmap_embed(&bm.0, bm.1, mods, info))
|
.embed(beatmap_embed(&bm.0, bm.1, &mods, info))
|
||||||
.components(vec![beatmap_components(msg.guild_id)])
|
.components(vec![beatmap_components(msg.guild_id)])
|
||||||
.reference_message(msg),
|
.reference_message(msg),
|
||||||
)
|
)
|
||||||
|
@ -683,14 +689,18 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
let mode = bm.1;
|
let mode = bm.1;
|
||||||
let mods = args.find::<Mods>().ok().unwrap_or_default();
|
let mods = args
|
||||||
|
.find::<UnparsedMods>()
|
||||||
|
.ok()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.to_mods(mode)?;
|
||||||
let style = args
|
let style = args
|
||||||
.single::<ScoreListStyle>()
|
.single::<ScoreListStyle>()
|
||||||
.unwrap_or(ScoreListStyle::Grid);
|
.unwrap_or(ScoreListStyle::Grid);
|
||||||
let username_arg = args.single::<UsernameArg>().ok();
|
let username_arg = args.single::<UsernameArg>().ok();
|
||||||
let user = to_user_id_query(username_arg, &env, msg.author.id).await?;
|
let user = to_user_id_query(username_arg, &env, msg.author.id).await?;
|
||||||
|
|
||||||
let scores = do_check(&env, &bm, mods, &user).await?;
|
let scores = do_check(&env, &bm, &mods, &user).await?;
|
||||||
|
|
||||||
if scores.is_empty() {
|
if scores.is_empty() {
|
||||||
msg.reply(&ctx, "No scores found").await?;
|
msg.reply(&ctx, "No scores found").await?;
|
||||||
|
@ -702,7 +712,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
||||||
format!(
|
format!(
|
||||||
"Here are the scores by `{}` on `{}`!",
|
"Here are the scores by `{}` on `{}`!",
|
||||||
&user,
|
&user,
|
||||||
bm.short_link(mods)
|
bm.short_link(&mods)
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
@ -716,7 +726,7 @@ pub async fn check(ctx: &Context, msg: &Message, mut args: Args) -> CommandResul
|
||||||
pub(crate) async fn do_check(
|
pub(crate) async fn do_check(
|
||||||
env: &OsuEnv,
|
env: &OsuEnv,
|
||||||
bm: &BeatmapWithMode,
|
bm: &BeatmapWithMode,
|
||||||
mods: Mods,
|
mods: &Mods,
|
||||||
user: &UserID,
|
user: &UserID,
|
||||||
) -> Result<Vec<Score>> {
|
) -> Result<Vec<Score>> {
|
||||||
let BeatmapWithMode(b, m) = bm;
|
let BeatmapWithMode(b, m) = bm;
|
||||||
|
@ -856,7 +866,7 @@ async fn get_user(
|
||||||
.oppai
|
.oppai
|
||||||
.get_beatmap(m.beatmap_id)
|
.get_beatmap(m.beatmap_id)
|
||||||
.await?
|
.await?
|
||||||
.get_info_with(mode, m.mods)?;
|
.get_info_with(mode, &m.mods)?;
|
||||||
Some((m, BeatmapWithMode(beatmap, mode), info))
|
Some((m, BeatmapWithMode(beatmap, mode), info))
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
|
@ -907,7 +917,7 @@ pub(in crate::discord) async fn calculate_weighted_map_length(
|
||||||
let beatmap = cache.get_beatmap(s.beatmap_id, mode).await?;
|
let beatmap = cache.get_beatmap(s.beatmap_id, mode).await?;
|
||||||
Ok(beatmap
|
Ok(beatmap
|
||||||
.difficulty
|
.difficulty
|
||||||
.apply_mods(s.mods, 0.0 /* dont care */)
|
.apply_mods(&s.mods, 0.0 /* dont care */)
|
||||||
.drain_length
|
.drain_length
|
||||||
.as_secs_f64()) as Result<_>
|
.as_secs_f64()) as Result<_>
|
||||||
})
|
})
|
||||||
|
|
|
@ -74,15 +74,20 @@ impl BeatmapContent {
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
combo: Option<usize>,
|
combo: Option<usize>,
|
||||||
accuracy: Accuracy,
|
accuracy: Accuracy,
|
||||||
mods: Mods,
|
mods: &Mods,
|
||||||
) -> Result<f64> {
|
) -> Result<f64> {
|
||||||
|
let clock = match mods.inner.clock_rate() {
|
||||||
|
None => bail!("cannot calculate pp for mods: {}", mods),
|
||||||
|
Some(clock) => clock as f64,
|
||||||
|
};
|
||||||
let mut perf = self
|
let mut perf = self
|
||||||
.content
|
.content
|
||||||
.performance()
|
.performance()
|
||||||
.mode_or_ignore(mode.into())
|
.mode_or_ignore(mode.into())
|
||||||
.accuracy(accuracy.into())
|
.accuracy(accuracy.into())
|
||||||
.misses(accuracy.misses() as u32)
|
.misses(accuracy.misses() as u32)
|
||||||
.mods(mods.bits() as u32);
|
.mods(mods.bits())
|
||||||
|
.clock_rate(clock);
|
||||||
if let Some(combo) = combo {
|
if let Some(combo) = combo {
|
||||||
perf = perf.combo(combo as u32);
|
perf = perf.combo(combo as u32);
|
||||||
}
|
}
|
||||||
|
@ -91,12 +96,17 @@ impl BeatmapContent {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get info given mods.
|
/// Get info given mods.
|
||||||
pub fn get_info_with(&self, mode: Mode, mods: Mods) -> Result<BeatmapInfo> {
|
pub fn get_info_with(&self, mode: Mode, mods: &Mods) -> Result<BeatmapInfo> {
|
||||||
|
let clock = match mods.inner.clock_rate() {
|
||||||
|
None => bail!("cannot calculate info for mods: {}", mods),
|
||||||
|
Some(clock) => clock as f64,
|
||||||
|
};
|
||||||
let attrs = self
|
let attrs = self
|
||||||
.content
|
.content
|
||||||
.performance()
|
.performance()
|
||||||
.mode_or_ignore(mode.into())
|
.mode_or_ignore(mode.into())
|
||||||
.mods(mods.bits() as u32)
|
.mods(mods.bits())
|
||||||
|
.clock_rate(clock)
|
||||||
.calculate();
|
.calculate();
|
||||||
Ok(BeatmapInfo {
|
Ok(BeatmapInfo {
|
||||||
objects: self.content.hit_objects.len(),
|
objects: self.content.hit_objects.len(),
|
||||||
|
@ -105,7 +115,7 @@ impl BeatmapContent {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_possible_pp_with(&self, mode: Mode, mods: Mods) -> Result<BeatmapInfoWithPP> {
|
pub fn get_possible_pp_with(&self, mode: Mode, mods: &Mods) -> Result<BeatmapInfoWithPP> {
|
||||||
let pp: [f64; 4] = [
|
let pp: [f64; 4] = [
|
||||||
self.get_pp_from(mode, None, Accuracy::ByValue(95.0, 0), mods)?,
|
self.get_pp_from(mode, None, Accuracy::ByValue(95.0, 0), mods)?,
|
||||||
self.get_pp_from(mode, None, Accuracy::ByValue(98.0, 0), mods)?,
|
self.get_pp_from(mode, None, Accuracy::ByValue(98.0, 0), mods)?,
|
||||||
|
|
|
@ -446,7 +446,7 @@ pub async fn get_leaderboard(
|
||||||
score.count_50,
|
score.count_50,
|
||||||
score.count_miss,
|
score.count_miss,
|
||||||
),
|
),
|
||||||
score.mods,
|
&score.mods,
|
||||||
)
|
)
|
||||||
.ok()
|
.ok()
|
||||||
.map(|v| (false, v))
|
.map(|v| (false, v))
|
||||||
|
|
|
@ -14,7 +14,7 @@ pub mod request;
|
||||||
|
|
||||||
/// Client is the client that will perform calls to the osu! api server.
|
/// Client is the client that will perform calls to the osu! api server.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Client {
|
pub struct OsuClient {
|
||||||
rosu: Arc<rosu_v2::Osu>,
|
rosu: Arc<rosu_v2::Osu>,
|
||||||
|
|
||||||
user_header_cache: Arc<Mutex<HashMap<u64, Option<UserHeader>>>>,
|
user_header_cache: Arc<Mutex<HashMap<u64, Option<UserHeader>>>>,
|
||||||
|
@ -30,15 +30,15 @@ pub fn vec_try_into<U, T: std::convert::TryFrom<U>>(v: Vec<U>) -> Result<Vec<T>,
|
||||||
Ok(res)
|
Ok(res)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Client {
|
impl OsuClient {
|
||||||
/// Create a new client from the given API key.
|
/// Create a new client from the given API key.
|
||||||
pub async fn new(client_id: u64, client_secret: impl Into<String>) -> Result<Client> {
|
pub async fn new(client_id: u64, client_secret: impl Into<String>) -> Result<OsuClient> {
|
||||||
let rosu = rosu_v2::OsuBuilder::new()
|
let rosu = rosu_v2::OsuBuilder::new()
|
||||||
.client_id(client_id)
|
.client_id(client_id)
|
||||||
.client_secret(client_secret)
|
.client_secret(client_secret)
|
||||||
.build()
|
.build()
|
||||||
.await?;
|
.await?;
|
||||||
Ok(Client {
|
Ok(OsuClient {
|
||||||
rosu: Arc::new(rosu),
|
rosu: Arc::new(rosu),
|
||||||
user_header_cache: Arc::new(Mutex::new(HashMap::new())),
|
user_header_cache: Arc::new(Mutex::new(HashMap::new())),
|
||||||
})
|
})
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
|
use rosu_v2::prelude::GameModIntermode;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
@ -85,39 +86,44 @@ impl Difficulty {
|
||||||
// then convert back
|
// then convert back
|
||||||
self.od = (79.0 - (hit_timing - 0.5)) / 6.0;
|
self.od = (79.0 - (hit_timing - 0.5)) / 6.0;
|
||||||
}
|
}
|
||||||
fn apply_length_by_ratio(&mut self, mul: u32, div: u32) {
|
fn apply_length_by_ratio(&mut self, ratio: f64) {
|
||||||
self.bpm = self.bpm / (mul as f64) * (div as f64); // Inverse since bpm increases while time decreases
|
self.bpm /= ratio; // Inverse since bpm increases while time decreases
|
||||||
self.drain_length = self.drain_length * mul / div;
|
self.drain_length = Duration::from_secs_f64(self.drain_length.as_secs_f64() * ratio);
|
||||||
self.total_length = self.total_length * mul / div;
|
self.total_length = Duration::from_secs_f64(self.total_length.as_secs_f64() * ratio);
|
||||||
}
|
}
|
||||||
/// Apply mods to the given difficulty.
|
/// Apply mods to the given difficulty.
|
||||||
/// Note that `stars`, `aim` and `speed` cannot be calculated from this alone.
|
/// Note that `stars`, `aim` and `speed` cannot be calculated from this alone.
|
||||||
pub fn apply_mods(&self, mods: Mods, updated_stars: f64) -> Difficulty {
|
pub fn apply_mods(&self, mods: &Mods, updated_stars: f64) -> Difficulty {
|
||||||
let mut diff = Difficulty {
|
let mut diff = Difficulty {
|
||||||
stars: updated_stars,
|
stars: updated_stars,
|
||||||
..self.clone()
|
..self.clone()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Apply mods one by one
|
// Apply mods one by one
|
||||||
if mods.contains(Mods::EZ) {
|
if mods.inner.contains_intermode(GameModIntermode::Easy) {
|
||||||
diff.apply_everything_by_ratio(0.5);
|
diff.apply_everything_by_ratio(0.5);
|
||||||
}
|
}
|
||||||
if mods.contains(Mods::HR) {
|
if mods.inner.contains_intermode(GameModIntermode::HardRock) {
|
||||||
let old_cs = diff.cs;
|
let old_cs = diff.cs;
|
||||||
diff.apply_everything_by_ratio(1.4);
|
diff.apply_everything_by_ratio(1.4);
|
||||||
// CS is changed by 1.3 tho
|
// CS is changed by 1.3 tho
|
||||||
diff.cs = old_cs * 1.3;
|
diff.cs = old_cs * 1.3;
|
||||||
}
|
}
|
||||||
if mods.contains(Mods::HT) {
|
if let Some(ratio) = mods.inner.clock_rate() {
|
||||||
diff.apply_ar_by_time_ratio(4.0 / 3.0);
|
diff.apply_length_by_ratio(1.0 / ratio as f64);
|
||||||
diff.apply_od_by_time_ratio(4.0 / 3.0);
|
diff.apply_ar_by_time_ratio(1.0 / ratio as f64);
|
||||||
diff.apply_length_by_ratio(4, 3);
|
diff.apply_od_by_time_ratio(1.0 / ratio as f64);
|
||||||
}
|
|
||||||
if mods.contains(Mods::DT) {
|
|
||||||
diff.apply_ar_by_time_ratio(2.0 / 3.0);
|
|
||||||
diff.apply_od_by_time_ratio(2.0 / 3.0);
|
|
||||||
diff.apply_length_by_ratio(2, 3);
|
|
||||||
}
|
}
|
||||||
|
// if mods.contains(Mods::HT) {
|
||||||
|
// diff.apply_ar_by_time_ratio(4.0 / 3.0);
|
||||||
|
// diff.apply_od_by_time_ratio(4.0 / 3.0);
|
||||||
|
// diff.apply_length_by_ratio(4, 3);
|
||||||
|
// }
|
||||||
|
// if mods.contains(Mods::DT) {
|
||||||
|
// diff.apply_ar_by_time_ratio(2.0 / 3.0);
|
||||||
|
// diff.apply_od_by_time_ratio(2.0 / 3.0);
|
||||||
|
// diff.apply_length_by_ratio(2, 3);
|
||||||
|
// }
|
||||||
|
|
||||||
diff
|
diff
|
||||||
}
|
}
|
||||||
|
@ -126,7 +132,7 @@ impl Difficulty {
|
||||||
pub fn format_info<'a>(
|
pub fn format_info<'a>(
|
||||||
&self,
|
&self,
|
||||||
mode: Mode,
|
mode: Mode,
|
||||||
mods: Mods,
|
mods: &Mods,
|
||||||
original_beatmap: impl Into<Option<&'a Beatmap>> + 'a,
|
original_beatmap: impl Into<Option<&'a Beatmap>> + 'a,
|
||||||
) -> String {
|
) -> String {
|
||||||
let original_beatmap = original_beatmap.into();
|
let original_beatmap = original_beatmap.into();
|
||||||
|
@ -146,7 +152,7 @@ impl Difficulty {
|
||||||
original_beatmap.download_link(BeatmapSite::Bancho),
|
original_beatmap.download_link(BeatmapSite::Bancho),
|
||||||
original_beatmap.download_link(BeatmapSite::Beatconnect),
|
original_beatmap.download_link(BeatmapSite::Beatconnect),
|
||||||
original_beatmap.download_link(BeatmapSite::Chimu),
|
original_beatmap.download_link(BeatmapSite::Chimu),
|
||||||
original_beatmap.short_link(Some(mode), Some(mods))
|
original_beatmap.short_link(Some(mode), mods)
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
.unwrap_or("**Uploaded**".to_owned()),
|
.unwrap_or("**Uploaded**".to_owned()),
|
||||||
|
@ -405,7 +411,7 @@ impl Beatmap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return a parsable short link.
|
/// Return a parsable short link.
|
||||||
pub fn short_link(&self, override_mode: Option<Mode>, mods: Option<Mods>) -> String {
|
pub fn short_link(&self, override_mode: Option<Mode>, mods: &Mods) -> String {
|
||||||
format!(
|
format!(
|
||||||
"/b/{}{}{}",
|
"/b/{}{}{}",
|
||||||
self.beatmap_id,
|
self.beatmap_id,
|
||||||
|
@ -413,8 +419,7 @@ impl Beatmap {
|
||||||
Some(mode) if mode != self.mode => format!("/{}", mode.as_str_new_site()),
|
Some(mode) if mode != self.mode => format!("/{}", mode.as_str_new_site()),
|
||||||
_ => "".to_owned(),
|
_ => "".to_owned(),
|
||||||
},
|
},
|
||||||
mods.map(|m| format!("{}", m))
|
mods.strip_lazer(override_mode.unwrap_or(Mode::Std))
|
||||||
.unwrap_or_else(|| "".to_owned()),
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -587,7 +592,7 @@ impl fmt::Display for Rank {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Serialize, Deserialize)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Score {
|
pub struct Score {
|
||||||
pub id: Option<u64>, // No id if you fail
|
pub id: Option<u64>, // No id if you fail
|
||||||
pub user_id: u64,
|
pub user_id: u64,
|
||||||
|
|
|
@ -1,169 +1,439 @@
|
||||||
use serde::{Deserialize, Serialize};
|
use regex::Regex;
|
||||||
|
use rosu::{GameModIntermode, GameMods};
|
||||||
|
use rosu_v2::model::mods as rosu;
|
||||||
|
use rosu_v2::prelude::GameModsIntermode;
|
||||||
|
use std::borrow::Cow;
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
use std::str::FromStr;
|
||||||
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
|
use crate::Mode;
|
||||||
|
|
||||||
const LAZER_TEXT: &str = "v2";
|
const LAZER_TEXT: &str = "v2";
|
||||||
|
|
||||||
bitflags::bitflags! {
|
lazy_static::lazy_static! {
|
||||||
/// The mods available to osu!
|
// Beatmap(set) hooks
|
||||||
#[derive(std::default::Default, Serialize, Deserialize)]
|
static ref MODS: Regex = Regex::new(
|
||||||
pub struct Mods: u64 {
|
// r"(?:https?://)?osu\.ppy\.sh/(?P<link_type>s|b|beatmaps)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>[0123]))?(?:\+(?P<mods>[A-Z]+))?"
|
||||||
const NF = 1 << 0;
|
r"^((\+?)(?P<mods>([A-Za-z0-9][A-Za-z])+))?(@(?P<clock>\d(\.\d+)?)x)?(v2)?$"
|
||||||
const EZ = 1 << 1;
|
).unwrap();
|
||||||
const TD = 1 << 2;
|
}
|
||||||
const HD = 1 << 3;
|
|
||||||
const HR = 1 << 4;
|
|
||||||
const SD = 1 << 5;
|
|
||||||
const DT = 1 << 6;
|
|
||||||
const RX = 1 << 7;
|
|
||||||
const HT = 1 << 8;
|
|
||||||
const NC = 1 << 9;
|
|
||||||
const FL = 1 << 10;
|
|
||||||
const AT = 1 << 11;
|
|
||||||
const SO = 1 << 12;
|
|
||||||
const AP = 1 << 13;
|
|
||||||
const PF = 1 << 14;
|
|
||||||
const KEY4 = 1 << 15; /* TODO: what are these abbreviated to? */
|
|
||||||
const KEY5 = 1 << 16;
|
|
||||||
const KEY6 = 1 << 17;
|
|
||||||
const KEY7 = 1 << 18;
|
|
||||||
const KEY8 = 1 << 19;
|
|
||||||
const FADEIN = 1 << 20;
|
|
||||||
const RANDOM = 1 << 21;
|
|
||||||
const CINEMA = 1 << 22;
|
|
||||||
const TARGET = 1 << 23;
|
|
||||||
const KEY9 = 1 << 24;
|
|
||||||
const KEYCOOP = 1 << 25;
|
|
||||||
const KEY1 = 1 << 26;
|
|
||||||
const KEY3 = 1 << 27;
|
|
||||||
const KEY2 = 1 << 28;
|
|
||||||
const SCOREV2 = 1 << 29;
|
|
||||||
|
|
||||||
// Made up flags
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
const LAZER = 1 << 59;
|
pub struct UnparsedMods {
|
||||||
const UNKNOWN = 1 << 60;
|
mods: Cow<'static, str>,
|
||||||
|
clock: Option<f32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for UnparsedMods {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self {
|
||||||
|
mods: "".into(),
|
||||||
|
clock: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Mods {
|
impl FromStr for UnparsedMods {
|
||||||
pub const NOMOD: Mods = Mods::empty();
|
type Err = String;
|
||||||
pub const TOUCH_DEVICE: Mods = Self::TD;
|
|
||||||
pub const NOVIDEO: Mods = Self::TD; /* never forget */
|
fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
|
||||||
pub const SPEED_CHANGING: Mods =
|
if s.is_empty() {
|
||||||
Mods::from_bits_truncate(Self::DT.bits | Self::HT.bits | Self::NC.bits);
|
return Ok(UnparsedMods::default());
|
||||||
pub const MAP_CHANGING: Mods =
|
}
|
||||||
Mods::from_bits_truncate(Self::HR.bits | Self::EZ.bits | Self::SPEED_CHANGING.bits);
|
let ms = match MODS.captures(s) {
|
||||||
|
Some(m) => m,
|
||||||
|
None => return Err(format!("invalid mods: {}", s)),
|
||||||
|
};
|
||||||
|
let mods = ms.name("mods").map(|v| v.as_str().to_owned());
|
||||||
|
if let Some(mods) = &mods {
|
||||||
|
if GameModsIntermode::try_from_acronyms(mods).is_none() {
|
||||||
|
return Err(format!("invalid mod sequence: {}", mods));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(Self {
|
||||||
|
mods: mods.map(|v| v.into()).unwrap_or("".into()),
|
||||||
|
clock: ms
|
||||||
|
.name("clock")
|
||||||
|
.map(|v| v.as_str().parse::<f32>().unwrap())
|
||||||
|
.filter(|v| *v > 0.0),
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const MODS_WITH_NAMES: &[(Mods, &str)] = &[
|
impl UnparsedMods {
|
||||||
(Mods::NF, "NF"),
|
/// Convert to [Mods].
|
||||||
(Mods::EZ, "EZ"),
|
pub fn to_mods(&self, mode: Mode) -> Result<Mods> {
|
||||||
(Mods::TD, "TD"),
|
use rosu_v2::prelude::*;
|
||||||
(Mods::HD, "HD"),
|
let mut mods = Mods::from_str(&self.mods, mode)?;
|
||||||
(Mods::HR, "HR"),
|
if let Some(clock) = self.clock {
|
||||||
(Mods::SD, "SD"),
|
let has_night_day_core = mods.inner.contains_intermode(GameModIntermode::Nightcore)
|
||||||
(Mods::DT, "DT"),
|
|| mods.inner.contains_intermode(GameModIntermode::Daycore);
|
||||||
(Mods::RX, "RX"),
|
mods.inner.remove_all_intermode([
|
||||||
(Mods::HT, "HT"),
|
GameModIntermode::Daycore,
|
||||||
(Mods::NC, "NC"),
|
GameModIntermode::Nightcore,
|
||||||
(Mods::FL, "FL"),
|
GameModIntermode::DoubleTime,
|
||||||
(Mods::AT, "AT"),
|
GameModIntermode::HalfTime,
|
||||||
(Mods::SO, "SO"),
|
]);
|
||||||
(Mods::AP, "AP"),
|
let mut speed_change = Some(clock);
|
||||||
(Mods::PF, "PF"),
|
let adjust_pitch: Option<bool> = None;
|
||||||
(Mods::KEY1, "1K"),
|
if clock < 1.0 {
|
||||||
(Mods::KEY2, "2K"),
|
speed_change = speed_change.filter(|v| *v != 0.75);
|
||||||
(Mods::KEY3, "3K"),
|
mods.inner.insert(if has_night_day_core {
|
||||||
(Mods::KEY4, "4K"),
|
match mode {
|
||||||
(Mods::KEY5, "5K"),
|
Mode::Std => GameMod::DaycoreOsu(DaycoreOsu { speed_change }),
|
||||||
(Mods::KEY6, "6K"),
|
Mode::Taiko => GameMod::DaycoreTaiko(DaycoreTaiko { speed_change }),
|
||||||
(Mods::KEY7, "7K"),
|
Mode::Catch => GameMod::DaycoreCatch(DaycoreCatch { speed_change }),
|
||||||
(Mods::KEY8, "8K"),
|
Mode::Mania => GameMod::DaycoreMania(DaycoreMania { speed_change }),
|
||||||
(Mods::KEY9, "9K"),
|
}
|
||||||
(Mods::UNKNOWN, "??"),
|
} else {
|
||||||
];
|
match mode {
|
||||||
|
Mode::Std => GameMod::HalfTimeOsu(HalfTimeOsu {
|
||||||
|
speed_change,
|
||||||
|
adjust_pitch,
|
||||||
|
}),
|
||||||
|
Mode::Taiko => GameMod::HalfTimeTaiko(HalfTimeTaiko {
|
||||||
|
speed_change,
|
||||||
|
adjust_pitch,
|
||||||
|
}),
|
||||||
|
Mode::Catch => GameMod::HalfTimeCatch(HalfTimeCatch {
|
||||||
|
speed_change,
|
||||||
|
adjust_pitch,
|
||||||
|
}),
|
||||||
|
Mode::Mania => GameMod::HalfTimeMania(HalfTimeMania {
|
||||||
|
speed_change,
|
||||||
|
adjust_pitch,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if clock > 1.0 {
|
||||||
|
speed_change = speed_change.filter(|v| *v != 1.5);
|
||||||
|
mods.inner.insert(if has_night_day_core {
|
||||||
|
match mode {
|
||||||
|
Mode::Std => GameMod::NightcoreOsu(NightcoreOsu { speed_change }),
|
||||||
|
Mode::Taiko => GameMod::NightcoreTaiko(NightcoreTaiko { speed_change }),
|
||||||
|
Mode::Catch => GameMod::NightcoreCatch(NightcoreCatch { speed_change }),
|
||||||
|
Mode::Mania => GameMod::NightcoreMania(NightcoreMania { speed_change }),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
match mode {
|
||||||
|
Mode::Std => GameMod::DoubleTimeOsu(DoubleTimeOsu {
|
||||||
|
speed_change,
|
||||||
|
adjust_pitch,
|
||||||
|
}),
|
||||||
|
Mode::Taiko => GameMod::DoubleTimeTaiko(DoubleTimeTaiko {
|
||||||
|
speed_change,
|
||||||
|
adjust_pitch,
|
||||||
|
}),
|
||||||
|
Mode::Catch => GameMod::DoubleTimeCatch(DoubleTimeCatch {
|
||||||
|
speed_change,
|
||||||
|
adjust_pitch,
|
||||||
|
}),
|
||||||
|
Mode::Mania => GameMod::DoubleTimeMania(DoubleTimeMania {
|
||||||
|
speed_change,
|
||||||
|
adjust_pitch,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(mods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, PartialEq, Default)]
|
||||||
|
pub struct Mods {
|
||||||
|
pub inner: GameMods,
|
||||||
|
}
|
||||||
|
|
||||||
impl Mods {
|
impl Mods {
|
||||||
// Return the string length of the string representation of the mods.
|
pub const NOMOD: &'static Mods = &Mods {
|
||||||
pub fn str_len(&self) -> usize {
|
inner: GameMods::new(),
|
||||||
let s = format!("{}", self);
|
};
|
||||||
s.len()
|
|
||||||
|
pub fn strip_lazer(&self, mode: Mode) -> Self {
|
||||||
|
let mut m = self.clone();
|
||||||
|
m.inner.insert(Self::classic_mod_of(mode));
|
||||||
|
m
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn classic_mod_of(mode: Mode) -> rosu::GameMod {
|
||||||
|
match mode {
|
||||||
|
Mode::Std => rosu::GameMod::ClassicOsu(rosu::generated_mods::ClassicOsu::default()),
|
||||||
|
Mode::Taiko => {
|
||||||
|
rosu::GameMod::ClassicTaiko(rosu::generated_mods::ClassicTaiko::default())
|
||||||
|
}
|
||||||
|
Mode::Catch => {
|
||||||
|
rosu::GameMod::ClassicCatch(rosu::generated_mods::ClassicCatch::default())
|
||||||
|
}
|
||||||
|
Mode::Mania => {
|
||||||
|
rosu::GameMod::ClassicMania(rosu::generated_mods::ClassicMania::default())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<GameMods> for Mods {
|
||||||
|
fn from(inner: GameMods) -> Self {
|
||||||
|
Self { inner }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bitflags::bitflags! {
|
||||||
|
// /// The mods available to osu!
|
||||||
|
// #[derive(std::default::Default, Serialize, Deserialize)]
|
||||||
|
// pub struct Mods: u64 {
|
||||||
|
// const NF = 1 << 0;
|
||||||
|
// const EZ = 1 << 1;
|
||||||
|
// const TD = 1 << 2;
|
||||||
|
// const HD = 1 << 3;
|
||||||
|
// const HR = 1 << 4;
|
||||||
|
// const SD = 1 << 5;
|
||||||
|
// const DT = 1 << 6;
|
||||||
|
// const RX = 1 << 7;
|
||||||
|
// const HT = 1 << 8;
|
||||||
|
// const NC = 1 << 9;
|
||||||
|
// const FL = 1 << 10;
|
||||||
|
// const AT = 1 << 11;
|
||||||
|
// const SO = 1 << 12;
|
||||||
|
// const AP = 1 << 13;
|
||||||
|
// const PF = 1 << 14;
|
||||||
|
// const KEY4 = 1 << 15; /* TODO: what are these abbreviated to? */
|
||||||
|
// const KEY5 = 1 << 16;
|
||||||
|
// const KEY6 = 1 << 17;
|
||||||
|
// const KEY7 = 1 << 18;
|
||||||
|
// const KEY8 = 1 << 19;
|
||||||
|
// const FADEIN = 1 << 20;
|
||||||
|
// const RANDOM = 1 << 21;
|
||||||
|
// const CINEMA = 1 << 22;
|
||||||
|
// const TARGET = 1 << 23;
|
||||||
|
// const KEY9 = 1 << 24;
|
||||||
|
// const KEYCOOP = 1 << 25;
|
||||||
|
// const KEY1 = 1 << 26;
|
||||||
|
// const KEY3 = 1 << 27;
|
||||||
|
// const KEY2 = 1 << 28;
|
||||||
|
// const SCOREV2 = 1 << 29;
|
||||||
|
|
||||||
|
// // Made up flags
|
||||||
|
// const LAZER = 1 << 59;
|
||||||
|
// const UNKNOWN = 1 << 60;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// impl Mods {
|
||||||
|
// pub const NOMOD: Mods = Mods::empty();
|
||||||
|
// pub const TOUCH_DEVICE: Mods = Self::TD;
|
||||||
|
// pub const NOVIDEO: Mods = Self::TD; /* never forget */
|
||||||
|
// pub const SPEED_CHANGING: Mods =
|
||||||
|
// Mods::from_bits_truncate(Self::DT.bits | Self::HT.bits | Self::NC.bits);
|
||||||
|
// pub const MAP_CHANGING: Mods =
|
||||||
|
// Mods::from_bits_truncate(Self::HR.bits | Self::EZ.bits | Self::SPEED_CHANGING.bits);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// const MODS_WITH_NAMES: &[(Mods, &str)] = &[
|
||||||
|
// (Mods::NF, "NF"),
|
||||||
|
// (Mods::EZ, "EZ"),
|
||||||
|
// (Mods::TD, "TD"),
|
||||||
|
// (Mods::HD, "HD"),
|
||||||
|
// (Mods::HR, "HR"),
|
||||||
|
// (Mods::SD, "SD"),
|
||||||
|
// (Mods::DT, "DT"),
|
||||||
|
// (Mods::RX, "RX"),
|
||||||
|
// (Mods::HT, "HT"),
|
||||||
|
// (Mods::NC, "NC"),
|
||||||
|
// (Mods::FL, "FL"),
|
||||||
|
// (Mods::AT, "AT"),
|
||||||
|
// (Mods::SO, "SO"),
|
||||||
|
// (Mods::AP, "AP"),
|
||||||
|
// (Mods::PF, "PF"),
|
||||||
|
// (Mods::KEY1, "1K"),
|
||||||
|
// (Mods::KEY2, "2K"),
|
||||||
|
// (Mods::KEY3, "3K"),
|
||||||
|
// (Mods::KEY4, "4K"),
|
||||||
|
// (Mods::KEY5, "5K"),
|
||||||
|
// (Mods::KEY6, "6K"),
|
||||||
|
// (Mods::KEY7, "7K"),
|
||||||
|
// (Mods::KEY8, "8K"),
|
||||||
|
// (Mods::KEY9, "9K"),
|
||||||
|
// (Mods::UNKNOWN, "??"),
|
||||||
|
// ];
|
||||||
|
|
||||||
|
impl Mods {
|
||||||
|
pub fn bits(&self) -> u32 {
|
||||||
|
self.inner.bits()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn contains(&self, other: &Mods) -> bool {
|
||||||
|
other
|
||||||
|
.inner
|
||||||
|
.iter()
|
||||||
|
.filter(|v| v.acronym().as_str() != "CL")
|
||||||
|
.all(|m| self.inner.contains(m))
|
||||||
|
}
|
||||||
// Format the mods into a string with padded size.
|
// Format the mods into a string with padded size.
|
||||||
pub fn to_string_padded(&self, size: usize) -> String {
|
pub fn to_string_padded(&self, size: usize) -> String {
|
||||||
let s = format!("{}", self);
|
let s = format!("{}", self);
|
||||||
let real_padded = size;
|
let real_padded = size;
|
||||||
format!("{:>mw$}", s, mw = real_padded)
|
format!("{:>mw$}", s, mw = real_padded)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get details on the mods, if they are present.
|
||||||
|
pub fn details(&self) -> Vec<String> {
|
||||||
|
use rosu::GameMod::*;
|
||||||
|
fn fmt_speed_change(
|
||||||
|
mod_name: &str,
|
||||||
|
speed_change: &Option<f32>,
|
||||||
|
adjust_pitch: &Option<bool>,
|
||||||
|
) -> Option<String> {
|
||||||
|
if speed_change.is_none() && adjust_pitch.is_none() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let mut s = format!("**{}**: ", mod_name);
|
||||||
|
let mut need_comma = false;
|
||||||
|
if let Some(speed) = speed_change {
|
||||||
|
s += &format!("speed **{:.2}x**", speed);
|
||||||
|
need_comma = true;
|
||||||
|
}
|
||||||
|
if let Some(true) = adjust_pitch {
|
||||||
|
if need_comma {
|
||||||
|
s += ", ";
|
||||||
|
}
|
||||||
|
s += "pitch **changed**"
|
||||||
|
}
|
||||||
|
Some(s)
|
||||||
|
}
|
||||||
|
self.inner
|
||||||
|
.iter()
|
||||||
|
.filter_map(|m| match m {
|
||||||
|
DoubleTimeOsu(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch),
|
||||||
|
DoubleTimeTaiko(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch),
|
||||||
|
DoubleTimeCatch(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch),
|
||||||
|
DoubleTimeMania(dt) => fmt_speed_change("DT", &dt.speed_change, &dt.adjust_pitch),
|
||||||
|
NightcoreOsu(dt) => fmt_speed_change("NC", &dt.speed_change, &None),
|
||||||
|
NightcoreTaiko(dt) => fmt_speed_change("NC", &dt.speed_change, &None),
|
||||||
|
NightcoreCatch(dt) => fmt_speed_change("NC", &dt.speed_change, &None),
|
||||||
|
NightcoreMania(dt) => fmt_speed_change("NC", &dt.speed_change, &None),
|
||||||
|
HalfTimeOsu(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch),
|
||||||
|
HalfTimeTaiko(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch),
|
||||||
|
HalfTimeCatch(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch),
|
||||||
|
HalfTimeMania(ht) => fmt_speed_change("HT", &ht.speed_change, &ht.adjust_pitch),
|
||||||
|
DaycoreOsu(ht) => fmt_speed_change("DC", &ht.speed_change, &None),
|
||||||
|
DaycoreTaiko(ht) => fmt_speed_change("DC", &ht.speed_change, &None),
|
||||||
|
DaycoreCatch(ht) => fmt_speed_change("DC", &ht.speed_change, &None),
|
||||||
|
DaycoreMania(ht) => fmt_speed_change("DC", &ht.speed_change, &None),
|
||||||
|
|
||||||
|
_ => None,
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
// let mut res: Vec<String> = vec![];
|
||||||
|
|
||||||
|
// for m in &self.inner {
|
||||||
|
// match m {
|
||||||
|
// DoubleTimeOsu(dt) =>
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// res
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::str::FromStr for Mods {
|
impl Mods {
|
||||||
type Err = String;
|
pub fn from_str(mut s: &str, mode: Mode) -> Result<Self> {
|
||||||
fn from_str(mut s: &str) -> Result<Self, Self::Err> {
|
|
||||||
let mut res = Self::default();
|
|
||||||
// Strip leading +
|
// Strip leading +
|
||||||
if s.starts_with('+') {
|
if s.starts_with('+') {
|
||||||
s = &s[1..];
|
s = &s[1..];
|
||||||
}
|
}
|
||||||
while s.len() >= 2 {
|
let intermode =
|
||||||
let (m, nw) = s.split_at(2);
|
GameModsIntermode::try_from_acronyms(s).ok_or_else(|| error!("Invalid mods: {}", s))?;
|
||||||
s = nw;
|
let mut inner = intermode
|
||||||
match &m.to_uppercase()[..] {
|
.try_with_mode(mode.into())
|
||||||
"NF" => res |= Mods::NF,
|
.ok_or_else(|| error!("Invalid mods for `{}`: {}", mode, intermode))?;
|
||||||
"EZ" => res |= Mods::EZ,
|
// Always add classic mod to `inner`
|
||||||
"TD" => res |= Mods::TD,
|
inner.insert(Self::classic_mod_of(mode));
|
||||||
"HD" => res |= Mods::HD,
|
if !inner.is_valid() {
|
||||||
"HR" => res |= Mods::HR,
|
return Err(error!("Incompatible mods found: {}", inner));
|
||||||
"SD" => res |= Mods::SD,
|
|
||||||
"DT" => res |= Mods::DT,
|
|
||||||
"RX" => res |= Mods::RX,
|
|
||||||
"HT" => res |= Mods::HT,
|
|
||||||
"NC" => res |= Mods::NC | Mods::DT,
|
|
||||||
"FL" => res |= Mods::FL,
|
|
||||||
"AT" => res |= Mods::AT,
|
|
||||||
"SO" => res |= Mods::SO,
|
|
||||||
"AP" => res |= Mods::AP,
|
|
||||||
"PF" => res |= Mods::PF | Mods::SD,
|
|
||||||
"1K" => res |= Mods::KEY1,
|
|
||||||
"2K" => res |= Mods::KEY2,
|
|
||||||
"3K" => res |= Mods::KEY3,
|
|
||||||
"4K" => res |= Mods::KEY4,
|
|
||||||
"5K" => res |= Mods::KEY5,
|
|
||||||
"6K" => res |= Mods::KEY6,
|
|
||||||
"7K" => res |= Mods::KEY7,
|
|
||||||
"8K" => res |= Mods::KEY8,
|
|
||||||
"9K" => res |= Mods::KEY9,
|
|
||||||
"??" => res |= Mods::UNKNOWN,
|
|
||||||
v => return Err(format!("{} is not a valid mod", v)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !s.is_empty() {
|
|
||||||
Err("String of odd length is not a mod string".to_owned())
|
|
||||||
} else {
|
|
||||||
Ok(res)
|
|
||||||
}
|
}
|
||||||
|
Ok(Self { inner })
|
||||||
|
// let mut res = GameModsIntermode::default();
|
||||||
|
// while s.len() >= 2 {
|
||||||
|
// let (m, nw) = s.split_at(2);
|
||||||
|
// s = nw;
|
||||||
|
// match &m.to_uppercase()[..] {
|
||||||
|
// "NF" => res.insert(GameModIntermode::NoFail),
|
||||||
|
// "EZ" => res.insert(GameModIntermode::Easy),
|
||||||
|
// "TD" => res.insert(GameModIntermode::TouchDevice),
|
||||||
|
// "HD" => res.insert(GameModIntermode::Hidden),
|
||||||
|
// "HR" => res.insert(GameModIntermode::HardRock),
|
||||||
|
// "SD" => res.insert(GameModIntermode::SuddenDeath),
|
||||||
|
// "DT" => res.insert(GameModIntermode::DoubleTime),
|
||||||
|
// "RX" => res.insert(GameModIntermode::Relax),
|
||||||
|
// "HT" => res.insert(GameModIntermode::HalfTime),
|
||||||
|
// "NC" => res.insert(GameModIntermode::Nightcore),
|
||||||
|
// "FL" => res.insert(GameModIntermode::Flashlight),
|
||||||
|
// "AT" => res.insert(GameModIntermode::Autopilot),
|
||||||
|
// "SO" => res.insert(GameModIntermode::SpunOut),
|
||||||
|
// "AP" => res.insert(GameModIntermode::Autoplay),
|
||||||
|
// "PF" => res.insert(GameModIntermode::Perfect),
|
||||||
|
// "1K" => res.insert(GameModIntermode::OneKey),
|
||||||
|
// "2K" => res.insert(GameModIntermode::TwoKeys),
|
||||||
|
// "3K" => res.insert(GameModIntermode::ThreeKeys),
|
||||||
|
// "4K" => res.insert(GameModIntermode::FourKeys),
|
||||||
|
// "5K" => res.insert(GameModIntermode::FiveKeys),
|
||||||
|
// "6K" => res.insert(GameModIntermode::SixKeys),
|
||||||
|
// "7K" => res.insert(GameModIntermode::SevenKeys),
|
||||||
|
// "8K" => res.insert(GameModIntermode::EightKeys),
|
||||||
|
// "9K" => res.insert(GameModIntermode::NineKeys),
|
||||||
|
// v => return Err(format!("{} is not a valid mod", v)),
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if !s.is_empty() {
|
||||||
|
// Err("String of odd length is not a mod string".to_owned())
|
||||||
|
// } else {
|
||||||
|
// Ok(Mods { inner: res })
|
||||||
|
// }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl fmt::Display for Mods {
|
impl fmt::Display for Mods {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||||
if !(*self & (Mods::all() ^ Mods::LAZER)).is_empty() {
|
let is_lazer = !self.inner.contains_intermode(GameModIntermode::Classic);
|
||||||
write!(f, "+")?;
|
let mods = if !is_lazer {
|
||||||
for p in MODS_WITH_NAMES.iter() {
|
let mut v = self.inner.clone();
|
||||||
if !self.contains(p.0) {
|
v.remove_intermode(GameModIntermode::Classic);
|
||||||
continue;
|
Cow::Owned(v)
|
||||||
}
|
} else {
|
||||||
match p.0 {
|
Cow::Borrowed(&self.inner)
|
||||||
Mods::DT if self.contains(Mods::NC) => continue,
|
};
|
||||||
Mods::SD if self.contains(Mods::PF) => continue,
|
if !mods.is_empty() {
|
||||||
_ => (),
|
write!(f, "+{}", mods)?;
|
||||||
};
|
}
|
||||||
write!(f, "{}", p.1)?;
|
if let Some(clock) = mods.clock_rate() {
|
||||||
|
if clock != 1.0 && clock != 1.5 && clock != 0.75 {
|
||||||
|
write!(f, "@{:.2}x", clock)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if self.contains(Mods::LAZER) {
|
if is_lazer {
|
||||||
write!(f, "{}", LAZER_TEXT)?;
|
write!(f, "{}", LAZER_TEXT)?;
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
// if !(*self & (Mods::all() ^ Mods::LAZER)).is_empty() {
|
||||||
|
// write!(f, "+")?;
|
||||||
|
// for p in MODS_WITH_NAMES.iter() {
|
||||||
|
// if !self.contains(p.0) {
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
// match p.0 {
|
||||||
|
// Mods::DT if self.contains(Mods::NC) => continue,
|
||||||
|
// Mods::SD if self.contains(Mods::PF) => continue,
|
||||||
|
// _ => (),
|
||||||
|
// };
|
||||||
|
// write!(f, "{}", p.1)?;
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// if self.contains(Mods::LAZER) {
|
||||||
|
// write!(f, "{}", LAZER_TEXT)?;
|
||||||
|
// }
|
||||||
|
// Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,7 +1,4 @@
|
||||||
use rosu_v2::model::{
|
use rosu_v2::model::{self as rosu};
|
||||||
self as rosu,
|
|
||||||
mods::{GameModIntermode, GameModsIntermode},
|
|
||||||
};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
@ -244,180 +241,180 @@ impl From<rosu::Grade> for Rank {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<Mods> for rosu::mods::GameModsIntermode {
|
// impl From<Mods> for rosu::mods::GameModsIntermode {
|
||||||
fn from(value: Mods) -> Self {
|
// fn from(value: Mods) -> Self {
|
||||||
let mut res = GameModsIntermode::new();
|
// let mut res = GameModsIntermode::new();
|
||||||
const MOD_MAP: &[(Mods, GameModIntermode)] = &[
|
// const MOD_MAP: &[(Mods, GameModIntermode)] = &[
|
||||||
(Mods::NF, GameModIntermode::NoFail),
|
// (Mods::NF, GameModIntermode::NoFail),
|
||||||
(Mods::EZ, GameModIntermode::Easy),
|
// (Mods::EZ, GameModIntermode::Easy),
|
||||||
(Mods::TD, GameModIntermode::TouchDevice),
|
// (Mods::TD, GameModIntermode::TouchDevice),
|
||||||
(Mods::HD, GameModIntermode::Hidden),
|
// (Mods::HD, GameModIntermode::Hidden),
|
||||||
(Mods::HR, GameModIntermode::HardRock),
|
// (Mods::HR, GameModIntermode::HardRock),
|
||||||
(Mods::SD, GameModIntermode::SuddenDeath),
|
// (Mods::SD, GameModIntermode::SuddenDeath),
|
||||||
(Mods::DT, GameModIntermode::DoubleTime),
|
// (Mods::DT, GameModIntermode::DoubleTime),
|
||||||
(Mods::RX, GameModIntermode::Relax),
|
// (Mods::RX, GameModIntermode::Relax),
|
||||||
(Mods::HT, GameModIntermode::HalfTime),
|
// (Mods::HT, GameModIntermode::HalfTime),
|
||||||
(Mods::NC, GameModIntermode::Nightcore),
|
// (Mods::NC, GameModIntermode::Nightcore),
|
||||||
(Mods::FL, GameModIntermode::Flashlight),
|
// (Mods::FL, GameModIntermode::Flashlight),
|
||||||
(Mods::AT, GameModIntermode::Autoplay),
|
// (Mods::AT, GameModIntermode::Autoplay),
|
||||||
(Mods::SO, GameModIntermode::SpunOut),
|
// (Mods::SO, GameModIntermode::SpunOut),
|
||||||
(Mods::AP, GameModIntermode::Autopilot),
|
// (Mods::AP, GameModIntermode::Autopilot),
|
||||||
(Mods::PF, GameModIntermode::Perfect),
|
// (Mods::PF, GameModIntermode::Perfect),
|
||||||
(Mods::KEY1, GameModIntermode::OneKey),
|
// (Mods::KEY1, GameModIntermode::OneKey),
|
||||||
(Mods::KEY2, GameModIntermode::TwoKeys),
|
// (Mods::KEY2, GameModIntermode::TwoKeys),
|
||||||
(Mods::KEY3, GameModIntermode::ThreeKeys),
|
// (Mods::KEY3, GameModIntermode::ThreeKeys),
|
||||||
(Mods::KEY4, GameModIntermode::FourKeys),
|
// (Mods::KEY4, GameModIntermode::FourKeys),
|
||||||
(Mods::KEY5, GameModIntermode::FiveKeys),
|
// (Mods::KEY5, GameModIntermode::FiveKeys),
|
||||||
(Mods::KEY6, GameModIntermode::SixKeys),
|
// (Mods::KEY6, GameModIntermode::SixKeys),
|
||||||
(Mods::KEY7, GameModIntermode::SevenKeys),
|
// (Mods::KEY7, GameModIntermode::SevenKeys),
|
||||||
(Mods::KEY8, GameModIntermode::EightKeys),
|
// (Mods::KEY8, GameModIntermode::EightKeys),
|
||||||
(Mods::KEY9, GameModIntermode::NineKeys),
|
// (Mods::KEY9, GameModIntermode::NineKeys),
|
||||||
];
|
// ];
|
||||||
for (m1, m2) in MOD_MAP {
|
// for (m1, m2) in MOD_MAP {
|
||||||
if value.contains(*m1) {
|
// if value.contains(*m1) {
|
||||||
res.insert(*m2);
|
// res.insert(*m2);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
if !value.contains(Mods::LAZER) {
|
// if !value.contains(Mods::LAZER) {
|
||||||
res.insert(GameModIntermode::Classic);
|
// res.insert(GameModIntermode::Classic);
|
||||||
}
|
// }
|
||||||
res
|
// res
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
impl From<rosu::mods::GameModsIntermode> for Mods {
|
// impl From<rosu::mods::GameModsIntermode> for Mods {
|
||||||
fn from(value: rosu_v2::prelude::GameModsIntermode) -> Self {
|
// fn from(value: rosu_v2::prelude::GameModsIntermode) -> Self {
|
||||||
let init = if value.contains(GameModIntermode::Classic) {
|
// let init = if value.contains(GameModIntermode::Classic) {
|
||||||
Mods::NOMOD
|
// Mods::NOMOD
|
||||||
} else {
|
// } else {
|
||||||
Mods::LAZER
|
// Mods::LAZER
|
||||||
};
|
// };
|
||||||
value
|
// value
|
||||||
.into_iter()
|
// .into_iter()
|
||||||
.map(|m| match m {
|
// .map(|m| match m {
|
||||||
GameModIntermode::NoFail => Mods::NF,
|
// GameModIntermode::NoFail => Mods::NF,
|
||||||
GameModIntermode::Easy => Mods::EZ,
|
// GameModIntermode::Easy => Mods::EZ,
|
||||||
GameModIntermode::TouchDevice => Mods::TD,
|
// GameModIntermode::TouchDevice => Mods::TD,
|
||||||
GameModIntermode::Hidden => Mods::HD,
|
// GameModIntermode::Hidden => Mods::HD,
|
||||||
GameModIntermode::HardRock => Mods::HR,
|
// GameModIntermode::HardRock => Mods::HR,
|
||||||
GameModIntermode::SuddenDeath => Mods::SD,
|
// GameModIntermode::SuddenDeath => Mods::SD,
|
||||||
GameModIntermode::DoubleTime => Mods::DT,
|
// GameModIntermode::DoubleTime => Mods::DT,
|
||||||
GameModIntermode::Relax => Mods::RX,
|
// GameModIntermode::Relax => Mods::RX,
|
||||||
GameModIntermode::HalfTime => Mods::HT,
|
// GameModIntermode::HalfTime => Mods::HT,
|
||||||
GameModIntermode::Nightcore => Mods::DT | Mods::NC,
|
// GameModIntermode::Nightcore => Mods::DT | Mods::NC,
|
||||||
GameModIntermode::Flashlight => Mods::FL,
|
// GameModIntermode::Flashlight => Mods::FL,
|
||||||
GameModIntermode::Autoplay => Mods::AT,
|
// GameModIntermode::Autoplay => Mods::AT,
|
||||||
GameModIntermode::SpunOut => Mods::SO,
|
// GameModIntermode::SpunOut => Mods::SO,
|
||||||
GameModIntermode::Autopilot => Mods::AP,
|
// GameModIntermode::Autopilot => Mods::AP,
|
||||||
GameModIntermode::Perfect => Mods::SD | Mods::PF,
|
// GameModIntermode::Perfect => Mods::SD | Mods::PF,
|
||||||
GameModIntermode::OneKey => Mods::KEY1,
|
// GameModIntermode::OneKey => Mods::KEY1,
|
||||||
GameModIntermode::TwoKeys => Mods::KEY2,
|
// GameModIntermode::TwoKeys => Mods::KEY2,
|
||||||
GameModIntermode::ThreeKeys => Mods::KEY3,
|
// GameModIntermode::ThreeKeys => Mods::KEY3,
|
||||||
GameModIntermode::FourKeys => Mods::KEY4,
|
// GameModIntermode::FourKeys => Mods::KEY4,
|
||||||
GameModIntermode::FiveKeys => Mods::KEY5,
|
// GameModIntermode::FiveKeys => Mods::KEY5,
|
||||||
GameModIntermode::SixKeys => Mods::KEY6,
|
// GameModIntermode::SixKeys => Mods::KEY6,
|
||||||
GameModIntermode::SevenKeys => Mods::KEY7,
|
// GameModIntermode::SevenKeys => Mods::KEY7,
|
||||||
GameModIntermode::EightKeys => Mods::KEY8,
|
// GameModIntermode::EightKeys => Mods::KEY8,
|
||||||
GameModIntermode::NineKeys => Mods::KEY9,
|
// GameModIntermode::NineKeys => Mods::KEY9,
|
||||||
GameModIntermode::Classic => Mods::NOMOD,
|
// GameModIntermode::Classic => Mods::NOMOD,
|
||||||
_ => Mods::UNKNOWN,
|
// _ => Mods::UNKNOWN,
|
||||||
})
|
// })
|
||||||
.fold(init, |a, b| a | b)
|
// .fold(init, |a, b| a | b)
|
||||||
|
|
||||||
// Mods::from_bits_truncate(value.bits() as u64)
|
// // Mods::from_bits_truncate(value.bits() as u64)
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
impl From<rosu::mods::GameMods> for Mods {
|
// impl From<rosu::mods::GameMods> for Mods {
|
||||||
fn from(value: rosu::mods::GameMods) -> Self {
|
// fn from(value: rosu::mods::GameMods) -> Self {
|
||||||
let unknown =
|
// let unknown =
|
||||||
rosu::mods::GameModIntermode::Unknown(rosu_v2::prelude::UnknownMod::default());
|
// rosu::mods::GameModIntermode::Unknown(rosu_v2::prelude::UnknownMod::default());
|
||||||
value
|
// value
|
||||||
.iter()
|
// .iter()
|
||||||
.cloned()
|
// .cloned()
|
||||||
.map(|m| match m {
|
// .map(|m| match m {
|
||||||
rosu::mods::GameMod::HalfTimeOsu(ht)
|
// rosu::mods::GameMod::HalfTimeOsu(ht)
|
||||||
if ht.speed_change.is_some_and(|v| v != 0.75) =>
|
// if ht.speed_change.is_some_and(|v| v != 0.75) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::DaycoreOsu(dc)
|
// rosu::mods::GameMod::DaycoreOsu(dc)
|
||||||
if dc.speed_change.is_some_and(|v| v != 0.75) =>
|
// if dc.speed_change.is_some_and(|v| v != 0.75) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::DaycoreOsu(_) => rosu::mods::GameModIntermode::HalfTime,
|
// rosu::mods::GameMod::DaycoreOsu(_) => rosu::mods::GameModIntermode::HalfTime,
|
||||||
rosu::mods::GameMod::DoubleTimeOsu(dt)
|
// rosu::mods::GameMod::DoubleTimeOsu(dt)
|
||||||
if dt.speed_change.is_some_and(|v| v != 1.5) =>
|
// if dt.speed_change.is_some_and(|v| v != 1.5) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::NightcoreOsu(nc)
|
// rosu::mods::GameMod::NightcoreOsu(nc)
|
||||||
if nc.speed_change.is_some_and(|v| v != 1.5) =>
|
// if nc.speed_change.is_some_and(|v| v != 1.5) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::HalfTimeTaiko(ht)
|
// rosu::mods::GameMod::HalfTimeTaiko(ht)
|
||||||
if ht.speed_change.is_some_and(|v| v != 0.75) =>
|
// if ht.speed_change.is_some_and(|v| v != 0.75) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::DaycoreTaiko(dc)
|
// rosu::mods::GameMod::DaycoreTaiko(dc)
|
||||||
if dc.speed_change.is_some_and(|v| v != 0.75) =>
|
// if dc.speed_change.is_some_and(|v| v != 0.75) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::DaycoreTaiko(_) => rosu::mods::GameModIntermode::HalfTime,
|
// rosu::mods::GameMod::DaycoreTaiko(_) => rosu::mods::GameModIntermode::HalfTime,
|
||||||
rosu::mods::GameMod::DoubleTimeTaiko(dt)
|
// rosu::mods::GameMod::DoubleTimeTaiko(dt)
|
||||||
if dt.speed_change.is_some_and(|v| v != 1.5) =>
|
// if dt.speed_change.is_some_and(|v| v != 1.5) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::NightcoreTaiko(nc)
|
// rosu::mods::GameMod::NightcoreTaiko(nc)
|
||||||
if nc.speed_change.is_some_and(|v| v != 1.5) =>
|
// if nc.speed_change.is_some_and(|v| v != 1.5) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::HalfTimeCatch(ht)
|
// rosu::mods::GameMod::HalfTimeCatch(ht)
|
||||||
if ht.speed_change.is_some_and(|v| v != 0.75) =>
|
// if ht.speed_change.is_some_and(|v| v != 0.75) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::DaycoreCatch(dc)
|
// rosu::mods::GameMod::DaycoreCatch(dc)
|
||||||
if dc.speed_change.is_some_and(|v| v != 0.75) =>
|
// if dc.speed_change.is_some_and(|v| v != 0.75) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::DaycoreCatch(_) => rosu::mods::GameModIntermode::HalfTime,
|
// rosu::mods::GameMod::DaycoreCatch(_) => rosu::mods::GameModIntermode::HalfTime,
|
||||||
rosu::mods::GameMod::DoubleTimeCatch(dt)
|
// rosu::mods::GameMod::DoubleTimeCatch(dt)
|
||||||
if dt.speed_change.is_some_and(|v| v != 1.5) =>
|
// if dt.speed_change.is_some_and(|v| v != 1.5) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::NightcoreCatch(nc)
|
// rosu::mods::GameMod::NightcoreCatch(nc)
|
||||||
if nc.speed_change.is_some_and(|v| v != 1.5) =>
|
// if nc.speed_change.is_some_and(|v| v != 1.5) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::HalfTimeMania(ht)
|
// rosu::mods::GameMod::HalfTimeMania(ht)
|
||||||
if ht.speed_change.is_some_and(|v| v != 0.75) =>
|
// if ht.speed_change.is_some_and(|v| v != 0.75) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::DaycoreMania(dc)
|
// rosu::mods::GameMod::DaycoreMania(dc)
|
||||||
if dc.speed_change.is_some_and(|v| v != 0.75) =>
|
// if dc.speed_change.is_some_and(|v| v != 0.75) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
rosu::mods::GameMod::DaycoreMania(_) => rosu::mods::GameModIntermode::HalfTime,
|
// rosu::mods::GameMod::DaycoreMania(_) => rosu::mods::GameModIntermode::HalfTime,
|
||||||
rosu::mods::GameMod::DoubleTimeMania(dt)
|
// rosu::mods::GameMod::DoubleTimeMania(dt)
|
||||||
if dt.speed_change.is_some_and(|v| v != 1.5) =>
|
// if dt.speed_change.is_some_and(|v| v != 1.5) =>
|
||||||
{
|
// {
|
||||||
unknown
|
// unknown
|
||||||
}
|
// }
|
||||||
_ => m.intermode(),
|
// _ => m.intermode(),
|
||||||
})
|
// })
|
||||||
.collect::<GameModsIntermode>()
|
// .collect::<GameModsIntermode>()
|
||||||
.into()
|
// .into()
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
use core::fmt;
|
use core::fmt;
|
||||||
|
|
||||||
use crate::models::{Mode, Mods};
|
use crate::models::{Mode, Mods};
|
||||||
use crate::Client;
|
use crate::OsuClient;
|
||||||
use rosu_v2::error::OsuError;
|
use rosu_v2::error::OsuError;
|
||||||
use youmubot_prelude::*;
|
use youmubot_prelude::*;
|
||||||
|
|
||||||
|
@ -58,6 +58,7 @@ pub mod builders {
|
||||||
|
|
||||||
use crate::models;
|
use crate::models;
|
||||||
|
|
||||||
|
use super::OsuClient;
|
||||||
use super::*;
|
use super::*;
|
||||||
/// A builder for a Beatmap request.
|
/// A builder for a Beatmap request.
|
||||||
pub struct BeatmapRequestBuilder {
|
pub struct BeatmapRequestBuilder {
|
||||||
|
@ -82,7 +83,7 @@ pub mod builders {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn build(self, client: &Client) -> Result<Vec<models::Beatmap>> {
|
pub(crate) async fn build(self, client: &OsuClient) -> Result<Vec<models::Beatmap>> {
|
||||||
Ok(match self.kind {
|
Ok(match self.kind {
|
||||||
BeatmapRequestKind::Beatmap(id) => {
|
BeatmapRequestKind::Beatmap(id) => {
|
||||||
match handle_not_found(client.rosu.beatmap().map_id(id as u32).await)? {
|
match handle_not_found(client.rosu.beatmap().map_id(id as u32).await)? {
|
||||||
|
@ -141,7 +142,7 @@ pub mod builders {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn build(self, client: &Client) -> Result<Option<models::User>> {
|
pub(crate) async fn build(self, client: &OsuClient) -> Result<Option<models::User>> {
|
||||||
let mut r = client.rosu.user(self.user);
|
let mut r = client.rosu.user(self.user);
|
||||||
if let Some(mode) = self.mode {
|
if let Some(mode) = self.mode {
|
||||||
r = r.mode(mode.into());
|
r = r.mode(mode.into());
|
||||||
|
@ -154,7 +155,7 @@ pub mod builders {
|
||||||
- time::Duration::DAY * self.event_days.unwrap_or(31);
|
- time::Duration::DAY * self.event_days.unwrap_or(31);
|
||||||
let mut events = handle_not_found(client.rosu.recent_activity(user.user_id).await)?
|
let mut events = handle_not_found(client.rosu.recent_activity(user.user_id).await)?
|
||||||
.unwrap_or(vec![]);
|
.unwrap_or(vec![]);
|
||||||
events.retain(|e| (now <= e.created_at));
|
events.retain(|e: &rosu_v2::model::event::Event| (now <= e.created_at));
|
||||||
let stats = user.statistics.take().unwrap();
|
let stats = user.statistics.take().unwrap();
|
||||||
Ok(Some(models::User::from_rosu(user, stats, events)))
|
Ok(Some(models::User::from_rosu(user, stats, events)))
|
||||||
}
|
}
|
||||||
|
@ -199,31 +200,29 @@ pub mod builders {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn build(self, client: &Client) -> Result<Vec<models::Score>> {
|
pub(crate) async fn build(self, osu: &OsuClient) -> Result<Vec<models::Score>> {
|
||||||
let scores = handle_not_found(match self.user {
|
let scores = handle_not_found(match self.user {
|
||||||
Some(user) => {
|
Some(user) => {
|
||||||
let mut r = client
|
let mut r = osu.rosu.beatmap_user_scores(self.beatmap_id as u32, user);
|
||||||
.rosu
|
|
||||||
.beatmap_user_scores(self.beatmap_id as u32, user);
|
|
||||||
if let Some(mode) = self.mode {
|
if let Some(mode) = self.mode {
|
||||||
r = r.mode(mode.into());
|
r = r.mode(mode.into());
|
||||||
}
|
}
|
||||||
match self.mods {
|
match self.mods {
|
||||||
Some(mods) => r.await.map(|mut ss| {
|
Some(mods) => r.await.map(|mut ss| {
|
||||||
let mods = GameModsIntermode::from(mods);
|
// let mods = GameModsIntermode::from(mods.inner);
|
||||||
ss.retain(|s| mods.iter().all(|m| s.mods.contains_intermode(m)));
|
ss.retain(|s| Mods::from(s.mods.clone()).contains(&mods));
|
||||||
ss
|
ss
|
||||||
}),
|
}),
|
||||||
None => r.await,
|
None => r.await,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
let mut r = client.rosu.beatmap_scores(self.beatmap_id as u32).global();
|
let mut r = osu.rosu.beatmap_scores(self.beatmap_id as u32).global();
|
||||||
if let Some(mode) = self.mode {
|
if let Some(mode) = self.mode {
|
||||||
r = r.mode(mode.into());
|
r = r.mode(mode.into());
|
||||||
}
|
}
|
||||||
if let Some(mods) = self.mods {
|
if let Some(mods) = self.mods {
|
||||||
r = r.mods(GameModsIntermode::from(mods));
|
r = r.mods(GameModsIntermode::from(mods.inner));
|
||||||
}
|
}
|
||||||
if let Some(limit) = self.limit {
|
if let Some(limit) = self.limit {
|
||||||
r = r.limit(limit as u32);
|
r = r.limit(limit as u32);
|
||||||
|
@ -268,7 +267,7 @@ pub mod builders {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn build(self, client: &Client) -> Result<Vec<models::Score>> {
|
pub(crate) async fn build(self, client: &OsuClient) -> Result<Vec<models::Score>> {
|
||||||
let scores = handle_not_found({
|
let scores = handle_not_found({
|
||||||
let mut r = client.rosu.user_scores(self.user);
|
let mut r = client.rosu.user_scores(self.user);
|
||||||
r = match self.score_type {
|
r = match self.score_type {
|
||||||
|
|
|
@ -142,7 +142,6 @@ impl AnnouncerHandler {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn run(self, client: &Client) -> AnnouncerRunner {
|
pub fn run(self, client: &Client) -> AnnouncerRunner {
|
||||||
|
|
||||||
AnnouncerRunner {
|
AnnouncerRunner {
|
||||||
cache_http: CacheAndHttp::from_client(client),
|
cache_http: CacheAndHttp::from_client(client),
|
||||||
data: client.data.clone(),
|
data: client.data.clone(),
|
||||||
|
|
|
@ -39,8 +39,6 @@ pub async fn setup_prelude(
|
||||||
// Set up the SQL client.
|
// Set up the SQL client.
|
||||||
data.insert::<crate::SQLClient>(sql_pool.clone());
|
data.insert::<crate::SQLClient>(sql_pool.clone());
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Env {
|
Env {
|
||||||
http: http_client,
|
http: http_client,
|
||||||
sql: sql_pool,
|
sql: sql_pool,
|
||||||
|
|
Loading…
Add table
Reference in a new issue