osu: Some lazer-related stat reconsideration (#56)

* Split lazer property to its own toggle

* Use lazer stats from API more verbatim in pp calculation

* Update CI to use 1.83

* Set rust-toolchain
This commit is contained in:
Natsu Kagami 2024-12-21 23:05:15 +00:00 committed by GitHub
parent 0d93d55cee
commit 51fa34a7bf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 344 additions and 296 deletions

View file

@ -15,10 +15,9 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@1.83.0
with: with:
toolchain: 1.78.0 components: rustfmt
components: rustfmt
- name: Run rustfmt - name: Run rustfmt
run: cargo fmt -- --check run: cargo fmt -- --check
check: check:
@ -26,18 +25,17 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@1.83.0
id: cargo id: cargo
with: with:
toolchain: 1.78.0 components: clippy
components: clippy - uses: actions/cache@v4
- uses: actions/cache@v2
with: with:
path: | path: |
~/.cargo/registry ~/.cargo/registry
~/.cargo/git ~/.cargo/git
target target
key: ${{ runner.os }}-rust-${{ steps.cargo.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.lock') }}-lint key: ${{ runner.os }}-rust-${{ steps.cargo.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }}-lint
- name: Run cargo check - name: Run cargo check
run: cargo check run: cargo check
env: env:
@ -50,19 +48,18 @@ jobs:
name: Test name: Test
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable - uses: dtolnay/rust-toolchain@1.83.0
id: cargo id: cargo
with: with:
toolchain: 1.78.0 components: clippy
components: clippy - uses: actions/cache@v4
- uses: actions/cache@v2
with: with:
path: | path: |
~/.cargo/registry ~/.cargo/registry
~/.cargo/git ~/.cargo/git
target target
key: ${{ runner.os }}-rust-${{ steps.cargo.outputs.rustc_hash }}-${{ hashFiles('**/Cargo.lock') }}-debug-build key: ${{ runner.os }}-rust-${{ steps.cargo.outputs.cachekey }}-${{ hashFiles('**/Cargo.lock') }}-debug-build
- name: Run cargo test - name: Run cargo test
run: cargo test run: cargo test
env: env:
@ -75,11 +72,6 @@ jobs:
- uses: cachix/install-nix-action@v27 - uses: cachix/install-nix-action@v27
with: with:
github_access_token: ${{ secrets.GITHUB_TOKEN }} github_access_token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@stable
id: cargo
with:
toolchain: 1.78.0
components: clippy
- uses: cachix/cachix-action@v15 - uses: cachix/cachix-action@v15
with: with:
name: natsukagami name: natsukagami

View file

@ -1 +1 @@
1.79.0 1.83.0

View file

@ -149,7 +149,7 @@ mod scores {
use youmubot_prelude::table_format::{table_formatting, Align}; use youmubot_prelude::table_format::{table_formatting, Align};
use youmubot_prelude::*; use youmubot_prelude::*;
use crate::discord::oppai_cache::Accuracy; use crate::discord::oppai_cache::Stats;
use crate::discord::{Beatmap, BeatmapInfo, OsuEnv}; use crate::discord::{Beatmap, BeatmapInfo, OsuEnv};
use crate::models::{Mode, Score}; use crate::models::{Mode, Score};
@ -211,9 +211,9 @@ 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((beatmap, info)) as Result<(Beatmap, Option<BeatmapInfo>)> Ok((beatmap, info)) as Result<(Beatmap, BeatmapInfo)>
}) })
.collect::<stream::FuturesOrdered<_>>() .collect::<stream::FuturesOrdered<_>>()
.map(|v| v.ok()) .map(|v| v.ok())
@ -226,28 +226,18 @@ mod scores {
Some(v) => Ok(v), Some(v) => Ok(v),
None => { None => {
let b = oppai.get_beatmap(p.beatmap_id).await?; let b = oppai.get_beatmap(p.beatmap_id).await?;
let r: Result<_> = Ok({ let pp = b.get_pp_from(
b.get_pp_from( mode,
mode, Some(p.max_combo),
Some(p.max_combo as usize), Stats::Raw(&p.statistics),
Accuracy::ByCount( &p.mods,
p.count_300, );
p.count_100, Ok(format!("{:.2}[?]", pp))
p.count_50,
p.count_miss,
),
&p.mods,
)
.ok()
.map(|pp| format!("{:.2}[?]", pp))
}
.unwrap_or_else(|| "-".to_owned()));
r
} }
} }
}) })
.collect::<stream::FuturesOrdered<_>>() .collect::<stream::FuturesOrdered<_>>()
.map(|v| v.unwrap_or_else(|_| "-".to_owned())) .map(|v: Result<_>| v.unwrap_or_else(|_| "-".to_owned()))
.collect::<Vec<String>>(); .collect::<Vec<String>>();
let (beatmaps, pps) = future::join(beatmaps, pps).await; let (beatmaps, pps) = future::join(beatmaps, pps).await;
@ -259,7 +249,7 @@ mod scores {
match p.rank { match p.rank {
crate::models::Rank::F => beatmaps[i] crate::models::Rank::F => beatmaps[i]
.as_ref() .as_ref()
.and_then(|(_, i)| i.map(|i| i.objects)) .map(|(_, i)| i.object_count)
.map(|total| { .map(|total| {
(p.count_300 + p.count_100 + p.count_50 + p.count_miss) as f64 (p.count_300 + p.count_100 + p.count_50 + p.count_miss) as f64
/ (total as f64) / (total as f64)
@ -287,7 +277,7 @@ mod scores {
b.map(|(beatmap, info)| { b.map(|(beatmap, info)| {
format!( format!(
"[{:.1}*] {} - {} [{}] ({})", "[{:.1}*] {} - {} [{}] ({})",
info.map(|i| i.stars).unwrap_or(beatmap.difficulty.stars), info.attrs.stars(),
beatmap.artist, beatmap.artist,
beatmap.title, beatmap.title,
beatmap.difficulty_name, beatmap.difficulty_name,
@ -406,7 +396,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)) .map(move |v| v.get_possible_pp_with(self.mode.unwrap_or(b.mode), &self.mods))
} }
} }
@ -435,11 +425,10 @@ mod beatmapset {
let map = &self.maps[page]; let map = &self.maps[page];
let info = match &self.infos[page] { let info = match &self.infos[page] {
Some(info) => *info, Some(info) => info,
None => { None => {
let info = self.get_beatmap_info(ctx, map).await?; let info = self.get_beatmap_info(ctx, map).await?;
self.infos[page] = Some(info); self.infos[page].insert(info)
info
} }
}; };
msg.edit(ctx, msg.edit(ctx,

View file

@ -1,9 +1,11 @@
use super::{BeatmapWithMode, UserExtras}; use super::{oppai_cache::Stats, BeatmapWithMode, UserExtras};
use crate::{ use crate::{
discord::oppai_cache::{Accuracy, BeatmapContent, BeatmapInfoWithPP}, discord::oppai_cache::{BeatmapContent, BeatmapInfoWithPP},
models::{Beatmap, Difficulty, Mode, Mods, Rank, Score, User}, models::{Beatmap, Difficulty, Mode, Mods, Rank, Score, User},
UserHeader, UserHeader,
}; };
use rosu_pp::osu::{OsuPerformanceAttributes, OsuScoreOrigin};
use rosu_v2::prelude::GameModIntermode;
use serenity::{ use serenity::{
all::CreateAttachment, all::CreateAttachment,
builder::{CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter}, builder::{CreateEmbed, CreateEmbedAuthor, CreateEmbedFooter},
@ -80,7 +82,7 @@ pub fn beatmap_offline_embed(
) -> 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();
let (info, pp) = b.get_possible_pp_with(m, mods)?; let (info, pp) = b.get_possible_pp_with(m, mods);
let total_length = if !bm.hit_objects.is_empty() { let total_length = if !bm.hit_objects.is_empty() {
Duration::from_millis( Duration::from_millis(
@ -105,7 +107,7 @@ pub fn beatmap_offline_embed(
}; };
let diff = Difficulty { let diff = Difficulty {
stars: info.stars, stars: info.attrs.stars(),
aim: None, // TODO: this is currently unused aim: None, // TODO: this is currently unused
speed: None, // TODO: this is currently unused speed: None, // TODO: this is currently unused
cs: bm.cs as f64, cs: bm.cs as f64,
@ -115,12 +117,12 @@ pub fn beatmap_offline_embed(
count_normal: circles, count_normal: circles,
count_slider: sliders, count_slider: sliders,
count_spinner: spinners, count_spinner: spinners,
max_combo: Some(info.max_combo as u64), max_combo: Some(info.attrs.max_combo() as u64),
bpm: bm.bpm(), bpm: bm.bpm(),
drain_length: total_length, // It's hard to calculate so maybe just skip... drain_length: total_length, // It's hard to calculate so maybe just skip...
total_length, total_length,
} }
.apply_mods(mods, info.stars); .apply_mods(mods, info.attrs.stars());
let mut embed = CreateEmbed::new() let mut embed = CreateEmbed::new()
.title(beatmap_title( .title(beatmap_title(
&metadata.artist, &metadata.artist,
@ -179,8 +181,13 @@ fn beatmap_title(
.build() .build()
} }
pub fn beatmap_embed(b: &'_ Beatmap, m: Mode, mods: &Mods, info: BeatmapInfoWithPP) -> CreateEmbed { pub fn beatmap_embed(
let diff = b.difficulty.apply_mods(mods, info.0.stars); b: &'_ Beatmap,
m: Mode,
mods: &Mods,
info: &BeatmapInfoWithPP,
) -> CreateEmbed {
let diff = b.difficulty.apply_mods(mods, info.0.attrs.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))
.author( .author(
@ -306,23 +313,18 @@ 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);
let stars = info let stars = info.attrs.stars();
.as_ref()
.map(|info| info.stars)
.unwrap_or(b.difficulty.stars);
let score_line = match s.rank { let score_line = match s.rank {
Rank::SS | Rank::SSH => "SS".to_string(), Rank::SS | Rank::SSH => "SS".to_string(),
_ if s.perfect => format!("{:.2}% FC", accuracy), _ if s.perfect => format!("{:.2}% FC", accuracy),
Rank::F => { Rank::F => {
let display = info let display = {
.map(|info| { let p = ((s.count_300 + s.count_100 + s.count_50 + s.count_miss) as f64)
((s.count_300 + s.count_100 + s.count_50 + s.count_miss) as f64) / (info.object_count as f64)
/ (info.objects as f64) * 100.0;
* 100.0 format!("FAILED @ {:.2}%", p)
}) };
.map(|p| format!("FAILED @ {:.2}%", p))
.unwrap_or_else(|| "FAILED".to_owned());
format!("{:.2}% {} combo [{}]", accuracy, s.max_combo, display) format!("{:.2}% {} combo [{}]", accuracy, s.max_combo, display)
} }
v => format!( v => format!(
@ -330,34 +332,30 @@ impl<'a> ScoreEmbedBuilder<'a> {
accuracy, s.max_combo, s.count_miss, v accuracy, s.max_combo, s.count_miss, v
), ),
}; };
let pp = s.pp.map(|pp| (pp, format!("{:.2}pp", pp))).or_else(|| { let pp =
content s.pp.map(|pp| (pp, format!("{:.2}pp", pp)))
.get_pp_from( .unwrap_or_else(|| {
mode, let pp = content.get_pp_from(
Some(s.max_combo as usize), mode,
Accuracy::ByCount(s.count_300, s.count_100, s.count_50, s.count_miss), Some(s.max_combo),
&s.mods, Stats::Raw(&s.statistics),
) &s.mods,
.ok() );
.map(|pp| (pp, format!("{:.2}pp [?]", pp))) (pp, format!("{:.2}pp [?]", pp))
}); });
let pp = if !s.perfect { let pp = if !s.perfect {
content let mut fc_stats = s.statistics.clone();
.get_pp_from( fc_stats.great += fc_stats.miss;
mode, fc_stats.miss = 0;
None, Some(content.get_pp_from(mode, None, Stats::Raw(&fc_stats), &s.mods))
Accuracy::ByCount(s.count_300 + s.count_miss, s.count_100, s.count_50, 0), .filter(|&v| pp.0 < v) /* must be larger than real pp */
&s.mods, .map(|value| {
) let (_, original) = &pp;
.ok() format!("{} ({:.2}pp if FC?)", original, value)
.filter(|&v| pp.as_ref().map(|&(origin, _)| origin < v).unwrap_or(false))
.and_then(|value| {
pp.as_ref()
.map(|(_, original)| format!("{} ({:.2}pp if FC?)", original, value))
}) })
.or_else(|| pp.map(|v| v.1)) .unwrap_or(pp.1)
} else { } else {
pp.map(|v| v.1) pp.1
}; };
let pp_gained = { let pp_gained = {
let effective_pp = s.effective_pp.or_else(|| { let effective_pp = s.effective_pp.or_else(|| {
@ -373,9 +371,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
_ => None, _ => None,
} }
}; };
let score_line = pp let score_line = format!("{} | {}", &score_line, pp);
.map(|pp| format!("{} | {}", &score_line, pp))
.unwrap_or(score_line);
let max_combo = b let max_combo = b
.difficulty .difficulty
.max_combo .max_combo
@ -438,7 +434,7 @@ impl<'a> ScoreEmbedBuilder<'a> {
"Score stats", "Score stats",
format!( format!(
"**{}** | {} | **{:.2}%**", "**{}** | {} | **{:.2}%**",
grouped_number(s.score.unwrap_or(s.normalized_score as u64)), grouped_number(s.score),
max_combo, max_combo,
accuracy accuracy
), ),
@ -468,78 +464,90 @@ pub(crate) struct FakeScore<'a> {
pub bm: &'a BeatmapWithMode, pub bm: &'a BeatmapWithMode,
pub content: &'a BeatmapContent, pub content: &'a BeatmapContent,
pub mods: Mods, pub mods: Mods,
pub n300: u32,
pub count_300: usize, pub n100: u32,
pub count_100: usize, pub n50: u32,
pub count_50: usize, pub nmiss: u32,
pub count_miss: usize, pub max_combo: Option<u32>,
pub count_slider_ends_missed: Option<usize>, // lazer only
pub max_combo: Option<usize>,
} }
impl<'a> FakeScore<'a> { impl<'a> FakeScore<'a> {
fn is_ss(&self, map_max_combo: usize) -> bool { fn score_origin(&self, attrs: &OsuPerformanceAttributes) -> OsuScoreOrigin {
self.is_fc(map_max_combo) if !self.mods.is_lazer {
&& self.count_100 OsuScoreOrigin::Stable
+ self.count_50 } else if self
+ self.count_miss .mods
+ self.count_slider_ends_missed.unwrap_or(0) .inner
== 0 .contains_intermode(GameModIntermode::Classic)
} {
fn is_fc(&self, map_max_combo: usize) -> bool { OsuScoreOrigin::WithoutSliderAcc {
match self.max_combo { max_large_ticks: attrs.difficulty.n_large_ticks,
None => self.count_miss == 0, max_small_ticks: attrs.difficulty.n_sliders,
Some(combo) => combo == map_max_combo - self.count_slider_ends_missed.unwrap_or(0), }
} else {
OsuScoreOrigin::WithSliderAcc {
max_large_ticks: attrs.difficulty.n_large_ticks,
max_slider_ends: attrs.difficulty.n_sliders,
}
} }
} }
fn accuracy(&self) -> f64 {
100.0 fn is_ss(&self, map_max_combo: u32) -> bool {
* (self.count_300 as f64 * 300.0 self.is_fc(map_max_combo) && self.n100.max(self.n50).max(self.nmiss) == 0
+ self.count_100 as f64 * 100.0
+ self.count_50 as f64 * 50.0)
/ ((self.count_300 + self.count_100 + self.count_50 + self.count_miss) as f64 * 300.0)
} }
fn is_fc(&self, map_max_combo: u32) -> bool {
self.max_combo.is_none_or(|x| x == map_max_combo) && self.nmiss == 0
}
// fn accuracy(&self) -> f64 {
// self.state.accuracy(self.score_origin())
// }
pub fn embed(self, ctx: &Context) -> Result<CreateEmbed> { pub fn embed(self, ctx: &Context) -> Result<CreateEmbed> {
let BeatmapWithMode(b, mode) = self.bm; let BeatmapWithMode(b, mode) = self.bm;
let info = self.content.get_info_with(*mode, &self.mods)?; let info = self.content.get_info_with(*mode, &self.mods);
let max_combo = self.max_combo.unwrap_or( let attrs = match &info.attrs {
info.max_combo - self.count_miss - self.count_slider_ends_missed.unwrap_or(0), rosu_pp::any::PerformanceAttributes::Osu(osu_performance_attributes) => {
); osu_performance_attributes
let acc = format!("{:.2}%", self.accuracy()); }
let score_line: Cow<str> = if self.is_ss(info.max_combo) { _ => unreachable!(),
};
let max_combo = self
.max_combo
.unwrap_or(info.attrs.max_combo() - self.nmiss);
let mut perf = attrs
.clone()
.performance()
.n300(self.n300)
.n100(self.n100)
.n50(self.n50)
.misses(self.nmiss)
.lazer(self.mods.is_lazer)
.mods(self.mods.inner.clone());
let state = perf.generate_state()?;
let accuracy = state.accuracy(self.score_origin(attrs)) * 100.0;
let acc = format!("{:.2}%", accuracy);
let score_line: Cow<str> = if self.is_ss(attrs.max_combo()) {
"SS".into() "SS".into()
} else if self.is_fc(info.max_combo) { } else if self.is_fc(attrs.max_combo()) {
format!("{} FC", acc).into() format!("{} FC", acc).into()
} else { } else {
format!("{} {}x {} miss", acc, max_combo, self.count_miss).into() format!("{} {}x {} miss", acc, max_combo, self.nmiss).into()
}; };
let pp = self.content.get_pp_from( let pp = perf.calculate()?.pp;
*mode, let pp_if_fc: Cow<str> = if self.is_fc(attrs.max_combo()) {
self.max_combo,
Accuracy::ByCount(
self.count_300 as u64,
self.count_100 as u64,
self.count_50 as u64,
self.count_miss as u64,
),
&self.mods,
)?;
let pp_if_fc: Cow<str> = if self.is_fc(info.max_combo) {
"".into() "".into()
} else { } else {
let pp = self.content.get_pp_from( let pp = self.content.get_pp_from(
*mode, *mode,
None, None,
Accuracy::ByCount( Stats::AccOnly {
(self.count_300 + self.count_miss) as u64, acc: accuracy,
self.count_100 as u64, misses: 0,
self.count_50 as u64, },
0,
),
&self.mods, &self.mods,
)?; );
format!(" ({:.2}pp if fc)", pp).into() format!(" ({:.2}pp if fc)", pp).into()
}; };
@ -554,7 +562,7 @@ impl<'a> FakeScore<'a> {
MessageBuilder::new() MessageBuilder::new()
.push_safe(&youmu.name) .push_safe(&youmu.name)
.push(" | ") .push(" | ")
.push(b.full_title(&self.mods, info.stars)) .push(b.full_title(&self.mods, attrs.stars()))
.push(" | ") .push(" | ")
.push(score_line) .push(score_line)
.push(" | ") .push(" | ")
@ -566,21 +574,26 @@ impl<'a> FakeScore<'a> {
.description(format!("**pp gained**: **{:.2}**pp", pp)) .description(format!("**pp gained**: **{:.2}**pp", pp))
.field( .field(
"Score stats", "Score stats",
format!("**{}** combo | **{}**", max_combo, acc), format!(
"**{}**/{} combo | **{}**",
max_combo,
attrs.max_combo(),
acc
),
true, true,
) )
.field( .field(
"300s | 100s | 50s | misses", "300s | 100s | 50s | misses",
format!( format!(
"**{}** | **{}** | **{}** | **{}**", "**{}** | **{}** | **{}** | **{}**",
self.count_300, self.count_100, self.count_50, self.count_miss self.n300, self.n100, self.n50, self.nmiss
), ),
true, true,
) )
.field( .field(
"Map stats", "Map stats",
b.difficulty b.difficulty
.apply_mods(&self.mods, info.stars) .apply_mods(&self.mods, attrs.stars())
.format_info(*mode, &self.mods, b), .format_info(*mode, &self.mods, b),
false, false,
) )
@ -693,7 +706,7 @@ pub(crate) fn user_embed(u: User, ex: UserExtras) -> CreateEmbed {
.push(format!( .push(format!(
"> {}", "> {}",
map.difficulty map.difficulty
.apply_mods(&v.mods, info.stars) .apply_mods(&v.mods, info.attrs.stars())
.format_info(mode, &v.mods, &map) .format_info(mode, &v.mods, &map)
.replace('\n', "\n> ") .replace('\n', "\n> ")
)) ))

View file

@ -127,7 +127,7 @@ pub fn dot_osu_hook<'a>(
crate::discord::embeds::beatmap_offline_embed( crate::discord::embeds::beatmap_offline_embed(
&beatmap, &beatmap,
m, /*For now*/ m, /*For now*/
&Mods::from_str(msg.content.trim(), m).unwrap_or_default(), &Mods::from_str(msg.content.trim(), m, false).unwrap_or_default(),
) )
.pls_ok() .pls_ok()
} }
@ -157,7 +157,8 @@ pub fn dot_osu_hook<'a>(
crate::discord::embeds::beatmap_offline_embed( crate::discord::embeds::beatmap_offline_embed(
&beatmap, &beatmap,
m, /*For now*/ m, /*For now*/
&Mods::from_str(msg.content.trim(), m).unwrap_or_default(), &Mods::from_str(msg.content.trim(), m, false)
.unwrap_or_default(),
) )
.pls_ok() .pls_ok()
}) })
@ -300,7 +301,7 @@ async fn handle_beatmap<'a, 'b>(
.push_mono_safe(link) .push_mono_safe(link)
.build(), .build(),
) )
.embed(beatmap_embed(beatmap, mode, &mods, info)) .embed(beatmap_embed(beatmap, mode, &mods, &info))
.components(vec![beatmap_components(mode, reply_to.guild_id)]) .components(vec![beatmap_components(mode, reply_to.guild_id)])
.reference_message(reply_to), .reference_message(reply_to),
) )

View file

@ -195,7 +195,7 @@ pub fn handle_simulate_button<'a>(
let b = &bm.0; let b = &bm.0;
let mode = bm.1; let mode = bm.1;
let content = env.oppai.get_beatmap(b.beatmap_id).await?; let content = env.oppai.get_beatmap(b.beatmap_id).await?;
let info = content.get_info_with(mode, Mods::NOMOD)?; let info = content.get_info_with(mode, Mods::NOMOD);
assert!(mode == Mode::Std); assert!(mode == Mode::Std);
@ -214,7 +214,7 @@ pub fn handle_simulate_button<'a>(
)) ))
.timeout(Duration::from_secs(300)) .timeout(Duration::from_secs(300))
.field(mk_input("Mods", "NM")) .field(mk_input("Mods", "NM"))
.field(mk_input("Max Combo", info.max_combo.to_string())) .field(mk_input("Max Combo", info.attrs.max_combo().to_string()))
.field(mk_input("100s", "0")) .field(mk_input("100s", "0"))
.field(mk_input("50s", "0")) .field(mk_input("50s", "0"))
.field(mk_input("Misses", "0")), .field(mk_input("Misses", "0")),
@ -254,30 +254,27 @@ async fn handle_simluate_query(
let mode = bm.1; let mode = bm.1;
let content = env.oppai.get_beatmap(b.beatmap_id).await?; let content = env.oppai.get_beatmap(b.beatmap_id).await?;
let score = { let score: FakeScore = {
let inputs = &query.inputs; let inputs = &query.inputs;
let (mods, max_combo, c100, c50, cmiss, csliderends) = ( let (mods, max_combo, c100, c50, cmiss) =
&inputs[0], &inputs[1], &inputs[2], &inputs[3], &inputs[4], "", (&inputs[0], &inputs[1], &inputs[2], &inputs[3], &inputs[4]);
);
let mods = UnparsedMods::from_str(mods) let mods = UnparsedMods::from_str(mods)
.map_err(|v| Error::msg(v))? .map_err(|v| Error::msg(v))?
.to_mods(mode)?; .to_mods(mode)?;
let info = content.get_info_with(mode, &mods)?; let info = content.get_info_with(mode, &mods);
let max_combo = max_combo.parse::<usize>().ok(); let max_combo = max_combo.parse::<u32>().ok();
let c100 = c100.parse::<usize>().unwrap_or(0); let n100 = c100.parse::<u32>().unwrap_or(0);
let c50 = c50.parse::<usize>().unwrap_or(0); let n50 = c50.parse::<u32>().unwrap_or(0);
let cmiss = cmiss.parse::<usize>().unwrap_or(0); let nmiss = cmiss.parse::<u32>().unwrap_or(0);
let c300 = info.objects - c100 - c50 - cmiss; let n300 = info.object_count as u32 - n100 - n50 - nmiss;
let csliderends = csliderends.parse::<usize>().ok();
FakeScore { FakeScore {
bm: &bm, bm: &bm,
content: &content, content: &content,
mods, mods,
count_300: c300, n300,
count_100: c100, n100,
count_50: c50, n50,
count_miss: cmiss, nmiss,
count_slider_ends_missed: csliderends,
max_combo, max_combo,
} }
}; };
@ -339,7 +336,7 @@ async fn handle_last_req(
.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_followup( comp.create_followup(
&ctx, &ctx,
serenity::all::CreateInteractionResponseFollowup::new() serenity::all::CreateInteractionResponseFollowup::new()
@ -347,7 +344,7 @@ async fn handle_last_req(
"Information for beatmap `{}`", "Information for beatmap `{}`",
bm.short_link(&mods) bm.short_link(&mods)
)) ))
.embed(beatmap_embed(b, *m, &mods, info)) .embed(beatmap_embed(b, *m, &mods, &info))
.components(vec![beatmap_components(bm.1, comp.guild_id)]), .components(vec![beatmap_components(bm.1, comp.guild_id)]),
) )
.await?; .await?;

View file

@ -23,13 +23,13 @@ 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|beatmaps)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>[0123]))?(?:\+(?P<mods>\S+\b))?" r"(?:https?://)?osu\.ppy\.sh/(?P<link_type>s|b|beatmaps)/(?P<id>\d+)(?:[\&\?]m=(?P<mode>[0123]))?(?:(?P<mods>v2|[[:^alpha:]]\S+\b))?"
).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>\S+\b))?" r"(?:https?://)?osu\.ppy\.sh/beatmapsets/(?P<set_id>\d+)/?(?:\#(?P<mode>osu|taiko|fruits|mania)(?:/(?P<beatmap_id>\d+)|/?))?(?:(?P<mods>v2|[[:^alpha:]]\S+\b))?"
).unwrap(); ).unwrap();
static ref SHORT_LINK_REGEX: Regex = Regex::new( static ref SHORT_LINK_REGEX: Regex = Regex::new(
r"(?:^|\s|\W)(?P<main>/b/(?P<id>\d+)(?:/(?P<mode>osu|taiko|fruits|mania))?(?:\+(?P<mods>\S+\b))?)" r"(?:^|\s|\W)(?P<main>/b/(?P<id>\d+)(?:/(?P<mode>osu|taiko|fruits|mania))?(?:(?P<mods>v2|[[:^alpha:]]\S+\b))?)"
).unwrap(); ).unwrap();
// Score hook // Score hook
@ -149,7 +149,7 @@ impl EmbedType {
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))? .map(|b| b.get_possible_pp_with(mode, &mods))?
}; };
Ok(Self::Beatmap(Box::new(bm), info, mods)) Ok(Self::Beatmap(Box::new(bm), info, mods))
} }

View file

@ -306,13 +306,13 @@ pub async fn save(ctx: &Context, msg: &Message, mut args: Args) -> CommandResult
.oppai .oppai
.get_beatmap(beatmap.beatmap_id) .get_beatmap(beatmap.beatmap_id)
.await? .await?
.get_possible_pp_with(mode, Mods::NOMOD)?; .get_possible_pp_with(mode, Mods::NOMOD);
let mut reply = reply.await?; let mut reply = reply.await?;
reply reply
.edit( .edit(
&ctx, &ctx,
EditMessage::new() EditMessage::new()
.embed(beatmap_embed(&beatmap, mode, Mods::NOMOD, info)) .embed(beatmap_embed(&beatmap, mode, Mods::NOMOD, &info))
.components(vec![beatmap_components(mode, msg.guild_id)]), .components(vec![beatmap_components(mode, msg.guild_id)]),
) )
.await?; .await?;
@ -492,7 +492,7 @@ impl UserExtras {
.oppai .oppai
.get_beatmap(s.beatmap_id) .get_beatmap(s.beatmap_id)
.await? .await?
.get_info_with(mode, &s.mods)?; .get_info_with(mode, &s.mods);
Some((s, BeatmapWithMode(beatmap, mode), info)) Some((s, BeatmapWithMode(beatmap, mode), info))
} else { } else {
None None
@ -828,13 +828,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(bm.1, msg.guild_id)]) .components(vec![beatmap_components(bm.1, msg.guild_id)])
.reference_message(msg), .reference_message(msg),
) )

View file

@ -3,8 +3,12 @@ use std::io::Read;
use std::sync::Arc; use std::sync::Arc;
use rosu_map::Beatmap as BeatmapMetadata; use rosu_map::Beatmap as BeatmapMetadata;
use rosu_pp::{Beatmap, GameMods}; use rosu_pp::{
any::{PerformanceAttributes, ScoreState},
Beatmap,
};
use rosu_v2::prelude::ScoreStatistics;
use youmubot_db_sql::{models::osu as models, Pool}; use youmubot_db_sql::{models::osu as models, Pool};
use youmubot_prelude::*; use youmubot_prelude::*;
@ -28,100 +32,125 @@ pub struct BeatmapBackground {
} }
/// the output of "one" oppai run. /// the output of "one" oppai run.
#[derive(Clone, Copy, Debug)] #[derive(Debug, Clone)]
pub struct BeatmapInfo { pub struct BeatmapInfo {
pub objects: usize, pub attrs: PerformanceAttributes,
pub max_combo: usize, pub object_count: usize,
pub stars: f64,
} }
#[derive(Clone, Copy, Debug)] /// Stats to be consumed by [BeatmapContent::get_pp_from].
pub enum Accuracy { pub enum Stats<'a> {
ByCount(u64, u64, u64, u64), Raw(&'a ScoreStatistics),
// 300 / 100 / 50 / misses AccOnly { acc: f64, misses: u32 },
#[allow(dead_code)]
ByValue(f64, u64),
}
impl From<Accuracy> for f64 {
fn from(val: Accuracy) -> Self {
match val {
Accuracy::ByValue(v, _) => v,
Accuracy::ByCount(n300, n100, n50, nmiss) => {
100.0 * ((6 * n300 + 2 * n100 + n50) as f64)
/ ((6 * (n300 + n100 + n50 + nmiss)) as f64)
}
}
}
}
impl Accuracy {
pub fn misses(&self) -> usize {
(match self {
Accuracy::ByCount(_, _, _, nmiss) => *nmiss,
Accuracy::ByValue(_, nmiss) => *nmiss,
}) as usize
}
} }
/// Beatmap Info with attached 95/98/99/100% FC pp. /// Beatmap Info with attached 95/98/99/100% FC pp.
pub type BeatmapInfoWithPP = (BeatmapInfo, [f64; 4]); pub type BeatmapInfoWithPP = (BeatmapInfo, [f64; 4]);
fn apply_mods(
perf: rosu_pp::Performance<'_>,
mods: impl Into<GameMods>,
) -> rosu_pp::Performance<'_> {
use rosu_pp::Performance::*;
match perf {
Osu(o) => Osu(o.mods(mods)),
Taiko(t) => Taiko(t.mods(mods)),
Catch(f) => Catch(f.mods(mods)),
Mania(m) => Mania(m.mods(mods)),
}
}
impl BeatmapContent { impl BeatmapContent {
/// Get pp given the combo and accuracy. /// Get pp given the combo and accuracy.
pub fn get_pp_from( pub fn get_pp_from(
&self, &self,
mode: Mode, mode: Mode,
combo: Option<usize>, combo: Option<u32>,
accuracy: Accuracy, stats: Stats<'_>,
mods: &Mods, mods: &Mods,
) -> Result<f64> { ) -> f64 {
let perf = self let perf = self
.content .content
.performance() .performance()
.mode_or_ignore(mode.into()) .mode_or_ignore(mode.into())
.accuracy(accuracy.into()) .lazer(mods.is_lazer)
.misses(accuracy.misses() as u32); .mods(mods.inner.clone());
let mut perf = apply_mods(perf, mods.inner.clone()); let perf = match stats {
if let Some(combo) = combo { Stats::Raw(stats) => {
perf = perf.combo(combo as u32); let max_combo =
} combo.unwrap_or_else(|| self.get_info_with(mode, mods).attrs.max_combo());
perf.state(Self::stats_to_state(stats, mode, max_combo))
}
Stats::AccOnly { acc, misses } => if let Some(combo) = combo {
perf.combo(combo)
} else {
perf
}
.accuracy(acc)
.misses(misses),
};
let attrs = perf.calculate(); let attrs = perf.calculate();
Ok(attrs.pp()) attrs.pp()
}
fn stats_to_state(stats: &ScoreStatistics, mode: Mode, max_combo: u32) -> ScoreState {
let legacy = stats.as_legacy(mode.into());
ScoreState {
max_combo,
osu_large_tick_hits: stats.large_tick_hit,
osu_small_tick_hits: stats.small_tick_hit,
slider_end_hits: stats.slider_tail_hit,
n_geki: stats.perfect,
n_katu: stats.good,
n300: legacy.count_300,
n100: legacy.count_100,
n50: legacy.count_50,
misses: legacy.count_miss,
}
} }
/// 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) -> BeatmapInfo {
let perf = self.content.performance().mode_or_ignore(mode.into()); let attrs = self
let attrs = apply_mods(perf, mods.inner.clone()).calculate(); .content
Ok(BeatmapInfo { .performance()
objects: self.content.hit_objects.len(), .mode_or_ignore(mode.into())
max_combo: attrs.max_combo() as usize, .mods(mods.inner.clone())
stars: attrs.stars(), .calculate();
}) let object_count = self.content.hit_objects.len();
BeatmapInfo {
attrs,
object_count,
}
} }
pub fn get_possible_pp_with(&self, mode: Mode, mods: &Mods) -> Result<BeatmapInfoWithPP> { pub fn get_possible_pp_with(&self, mode: Mode, mods: &Mods) -> 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(
self.get_pp_from(mode, None, Accuracy::ByValue(98.0, 0), mods)?, mode,
self.get_pp_from(mode, None, Accuracy::ByValue(99.0, 0), mods)?, None,
self.get_pp_from(mode, None, Accuracy::ByValue(100.0, 0), mods)?, Stats::AccOnly {
acc: 95.0,
misses: 0,
},
mods,
),
self.get_pp_from(
mode,
None,
Stats::AccOnly {
acc: 98.0,
misses: 0,
},
mods,
),
self.get_pp_from(
mode,
None,
Stats::AccOnly {
acc: 99.0,
misses: 0,
},
mods,
),
self.get_pp_from(
mode,
None,
Stats::AccOnly {
acc: 100.0,
misses: 0,
},
mods,
),
]; ];
Ok((self.get_info_with(mode, mods)?, pp)) (self.get_info_with(mode, mods), pp)
} }
} }

View file

@ -24,7 +24,7 @@ use youmubot_prelude::{
}; };
use crate::{ use crate::{
discord::{db::OsuUser, display::ScoreListStyle, oppai_cache::Accuracy, BeatmapWithMode}, discord::{db::OsuUser, display::ScoreListStyle, oppai_cache::Stats, BeatmapWithMode},
models::{Mode, Mods}, models::{Mode, Mods},
request::UserID, request::UserID,
Score, Score,
@ -454,29 +454,24 @@ pub async fn get_leaderboard(
let mem = Arc::new(mem); let mem = Arc::new(mem);
scores scores
.into_iter() .into_iter()
.filter_map(|score| { .map(|score| {
let pp = score.pp.map(|v| (true, v)).or_else(|| { let pp = score.pp.map(|v| (true, v)).unwrap_or_else(|| {
oppai_map (
.get_pp_from( false,
oppai_map.get_pp_from(
*mode, *mode,
Some(score.max_combo as usize), Some(score.max_combo),
Accuracy::ByCount( Stats::Raw(&score.statistics),
score.count_300,
score.count_100,
score.count_50,
score.count_miss,
),
&score.mods, &score.mods,
) ),
.ok() )
.map(|v| (false, v)) });
})?; Ranking {
Some(Ranking {
pp: pp.1, pp: pp.1,
official: pp.0, official: pp.0,
member: mem.clone(), member: mem.clone(),
score, score,
}) }
}) })
.collect::<Vec<_>>() .collect::<Vec<_>>()
}) })
@ -520,7 +515,7 @@ pub async fn display_rankings_table(
bm: &BeatmapWithMode, bm: &BeatmapWithMode,
order: OrderBy, order: OrderBy,
) -> Result<()> { ) -> Result<()> {
let has_lazer_score = scores.iter().any(|v| v.score.score.is_none()); let has_lazer_score = scores.iter().any(|v| v.score.mods.is_lazer);
const ITEMS_PER_PAGE: usize = 5; const ITEMS_PER_PAGE: usize = 5;
let total_len = scores.len(); let total_len = scores.len();
@ -565,7 +560,7 @@ pub async fn display_rankings_table(
crate::discord::embeds::grouped_number(if has_lazer_score { crate::discord::embeds::grouped_number(if has_lazer_score {
score.normalized_score as u64 score.normalized_score as u64
} else { } else {
score.score.unwrap() score.score
}) })
} }
}, },

View file

@ -1,6 +1,6 @@
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use mods::Stats; use mods::Stats;
use rosu_v2::prelude::GameModIntermode; use rosu_v2::prelude::{GameModIntermode, ScoreStatistics};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use std::borrow::Cow; use std::borrow::Cow;
use std::fmt; use std::fmt;
@ -636,7 +636,7 @@ pub struct Score {
pub replay_available: bool, pub replay_available: bool,
pub beatmap_id: u64, pub beatmap_id: u64,
pub score: Option<u64>, pub score: u64,
pub normalized_score: u32, pub normalized_score: u32,
pub pp: Option<f64>, pub pp: Option<f64>,
pub rank: Rank, pub rank: Rank,
@ -649,9 +649,12 @@ pub struct Score {
pub count_miss: u64, pub count_miss: u64,
pub count_katu: u64, pub count_katu: u64,
pub count_geki: u64, pub count_geki: u64,
pub max_combo: u64, pub max_combo: u32,
pub perfect: bool, pub perfect: bool,
// lazer stats
pub statistics: ScoreStatistics,
/// Whether score would get pp /// Whether score would get pp
pub ranked: Option<bool>, pub ranked: Option<bool>,
/// Whether score would be stored /// Whether score would be stored

View file

@ -16,13 +16,14 @@ lazy_static::lazy_static! {
// Beatmap(set) hooks // Beatmap(set) hooks
static ref MODS: Regex = Regex::new( 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"(?: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)?(?P<stats>(@(ar|AR|od|OD|cs|CS|hp|HP)\d(\.\d)?)+)?(v2)?$" r"^((\+?)(?P<mods>([A-Za-z0-9][A-Za-z])+))?(@(?P<clock>\d(\.\d+)?)x)?(?P<stats>(@(ar|AR|od|OD|cs|CS|hp|HP)\d(\.\d)?)+)?(?P<lazer>v2)?$"
).unwrap(); ).unwrap();
} }
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct UnparsedMods { pub struct UnparsedMods {
mods: Cow<'static, str>, mods: Cow<'static, str>,
is_lazer: bool,
clock: Option<f64>, clock: Option<f64>,
} }
@ -43,8 +44,10 @@ impl FromStr for UnparsedMods {
return Err(format!("invalid mod sequence: {}", mods)); return Err(format!("invalid mod sequence: {}", mods));
} }
} }
let is_lazer = ms.name("lazer").is_some();
Ok(Self { Ok(Self {
mods: mods.map(|v| v.into()).unwrap_or("".into()), mods: mods.map(|v| v.into()).unwrap_or("".into()),
is_lazer,
clock: ms clock: ms
.name("clock") .name("clock")
.map(|v| v.as_str().parse::<_>().unwrap()) .map(|v| v.as_str().parse::<_>().unwrap())
@ -57,7 +60,7 @@ impl UnparsedMods {
/// Convert to [Mods]. /// Convert to [Mods].
pub fn to_mods(&self, mode: Mode) -> Result<Mods> { pub fn to_mods(&self, mode: Mode) -> Result<Mods> {
use rosu_v2::prelude::*; use rosu_v2::prelude::*;
let mut mods = Mods::from_str(&self.mods, mode)?; let mut mods = Mods::from_str(&self.mods, mode, self.is_lazer)?;
if let Some(clock) = self.clock { if let Some(clock) = self.clock {
let has_night_day_core = mods.inner.contains_intermode(GameModIntermode::Nightcore) let has_night_day_core = mods.inner.contains_intermode(GameModIntermode::Nightcore)
|| mods.inner.contains_intermode(GameModIntermode::Daycore); || mods.inner.contains_intermode(GameModIntermode::Daycore);
@ -71,6 +74,9 @@ impl UnparsedMods {
let adjust_pitch: Option<bool> = None; let adjust_pitch: Option<bool> = None;
if clock < 1.0 { if clock < 1.0 {
speed_change = speed_change.filter(|v| *v != 0.75); speed_change = speed_change.filter(|v| *v != 0.75);
if speed_change.is_some() {
mods.is_lazer = true;
}
mods.inner.insert(if has_night_day_core { mods.inner.insert(if has_night_day_core {
match mode { match mode {
Mode::Std => GameMod::DaycoreOsu(DaycoreOsu { speed_change }), Mode::Std => GameMod::DaycoreOsu(DaycoreOsu { speed_change }),
@ -101,6 +107,9 @@ impl UnparsedMods {
} }
if clock > 1.0 { if clock > 1.0 {
speed_change = speed_change.filter(|v| *v != 1.5); speed_change = speed_change.filter(|v| *v != 1.5);
if speed_change.is_some() {
mods.is_lazer = true;
}
mods.inner.insert(if has_night_day_core { mods.inner.insert(if has_night_day_core {
match mode { match mode {
Mode::Std => GameMod::NightcoreOsu(NightcoreOsu { speed_change }), Mode::Std => GameMod::NightcoreOsu(NightcoreOsu { speed_change }),
@ -137,6 +146,7 @@ impl UnparsedMods {
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct Mods { pub struct Mods {
pub inner: GameMods, pub inner: GameMods,
pub is_lazer: bool,
} }
/// Store overrides to the stats. /// Store overrides to the stats.
@ -161,6 +171,7 @@ impl Stats {
impl Mods { impl Mods {
pub const NOMOD: &'static Mods = &Mods { pub const NOMOD: &'static Mods = &Mods {
inner: GameMods::new(), inner: GameMods::new(),
is_lazer: false,
}; };
// fn classic_mod_of(mode: Mode) -> rosu::GameMod { // fn classic_mod_of(mode: Mode) -> rosu::GameMod {
@ -209,11 +220,11 @@ impl Mods {
} }
} }
impl From<GameMods> for Mods { // impl From<GameMods> for Mods {
fn from(inner: GameMods) -> Self { // fn from(inner: GameMods) -> Self {
Self { inner } // Self { inner }
} // }
} // }
// bitflags::bitflags! { // bitflags::bitflags! {
// /// The mods available to osu! // /// The mods available to osu!
@ -405,7 +416,18 @@ impl Mods {
} }
impl Mods { impl Mods {
pub fn from_str(mut s: &str, mode: Mode) -> Result<Self> { pub fn from_gamemods(mods: GameMods, is_lazer: bool) -> Self {
let is_lazer = is_lazer || {
let mut mm = mods.clone();
mm.remove_intermode(GameModIntermode::Classic);
mm.try_as_legacy().is_none()
};
Self {
inner: mods,
is_lazer,
}
}
pub fn from_str(mut s: &str, mode: Mode, is_lazer: bool) -> Result<Self> {
// Strip leading + // Strip leading +
if s.starts_with('+') { if s.starts_with('+') {
s = &s[1..]; s = &s[1..];
@ -420,7 +442,7 @@ impl Mods {
if !inner.is_valid() { if !inner.is_valid() {
return Err(error!("Incompatible mods found: {}", inner)); return Err(error!("Incompatible mods found: {}", inner));
} }
Ok(Self { inner }) Ok(Self::from_gamemods(inner, is_lazer))
// let mut res = GameModsIntermode::default(); // let mut res = GameModsIntermode::default();
// while s.len() >= 2 { // while s.len() >= 2 {
// let (m, nw) = s.split_at(2); // let (m, nw) = s.split_at(2);
@ -463,8 +485,7 @@ impl Mods {
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 {
let is_lazer = !self.inner.contains_intermode(GameModIntermode::Classic); let mods = if !self.is_lazer {
let mods = if !is_lazer {
let mut v = self.inner.clone(); let mut v = self.inner.clone();
v.remove_intermode(GameModIntermode::Classic); v.remove_intermode(GameModIntermode::Classic);
Cow::Owned(v) Cow::Owned(v)
@ -479,7 +500,7 @@ impl fmt::Display for Mods {
write!(f, "@{:.2}x", clock)?; write!(f, "@{:.2}x", clock)?;
} }
} }
if is_lazer { if self.is_lazer {
write!(f, "{}", LAZER_TEXT)?; write!(f, "{}", LAZER_TEXT)?;
} }
Ok(()) Ok(())

View file

@ -112,13 +112,18 @@ impl From<rosu::event::Event> for UserEvent {
impl From<rosu::score::Score> for Score { impl From<rosu::score::Score> for Score {
fn from(s: rosu::score::Score) -> Self { fn from(s: rosu::score::Score) -> Self {
let legacy_stats = s.statistics.as_legacy(s.mode); let legacy_stats = s.statistics.as_legacy(s.mode);
let score = if s.set_on_lazer {
s.score
} else {
s.classic_score
} as u64;
Self { Self {
id: Some(s.id), id: Some(s.id),
user_id: s.user_id as u64, user_id: s.user_id as u64,
date: time_to_utc(s.ended_at), date: time_to_utc(s.ended_at),
replay_available: s.replay, replay_available: s.replay,
beatmap_id: s.map_id as u64, beatmap_id: s.map_id as u64,
score: Some(s.legacy_score as u64).filter(|v| *v > 0), score,
normalized_score: s.score, normalized_score: s.score,
pp: s.pp.map(|v| v as f64), pp: s.pp.map(|v| v as f64),
rank: if s.passed { s.grade.into() } else { Rank::F }, rank: if s.passed { s.grade.into() } else { Rank::F },
@ -126,14 +131,15 @@ impl From<rosu::score::Score> for Score {
global_rank: s.rank_global, global_rank: s.rank_global,
effective_pp: s.weight.map(|w| w.pp as f64), effective_pp: s.weight.map(|w| w.pp as f64),
mode: s.mode.into(), mode: s.mode.into(),
mods: s.mods.into(), mods: Mods::from_gamemods(s.mods, s.set_on_lazer),
count_300: legacy_stats.count_300 as u64, count_300: legacy_stats.count_300 as u64,
count_100: legacy_stats.count_100 as u64, count_100: legacy_stats.count_100 as u64,
count_50: legacy_stats.count_50 as u64, count_50: legacy_stats.count_50 as u64,
count_miss: legacy_stats.count_miss as u64, count_miss: legacy_stats.count_miss as u64,
count_katu: legacy_stats.count_katu as u64, count_katu: legacy_stats.count_katu as u64,
count_geki: legacy_stats.count_geki as u64, count_geki: legacy_stats.count_geki as u64,
max_combo: s.max_combo as u64, statistics: s.statistics,
max_combo: s.max_combo,
perfect: s.is_perfect_combo, perfect: s.is_perfect_combo,
ranked: s.ranked, ranked: s.ranked,
preserved: s.preserve, preserved: s.preserve,

View file

@ -210,7 +210,9 @@ pub mod builders {
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.inner); // let mods = GameModsIntermode::from(mods.inner);
ss.retain(|s| Mods::from(s.mods.clone()).contains(&mods)); ss.retain(|s| {
Mods::from_gamemods(s.mods.clone(), s.set_on_lazer).contains(&mods)
});
ss ss
}), }),
None => r.await, None => r.await,