mirror of
https://github.com/natsukagami/youmubot.git
synced 2025-04-19 00:38:54 +00:00
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:
parent
0d93d55cee
commit
51fa34a7bf
14 changed files with 344 additions and 296 deletions
30
.github/workflows/build_test.yml
vendored
30
.github/workflows/build_test.yml
vendored
|
@ -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
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
1.79.0
|
1.83.0
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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> ")
|
||||||
))
|
))
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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?;
|
||||||
|
|
|
@ -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))
|
||||||
}
|
}
|
||||||
|
|
|
@ -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),
|
||||||
)
|
)
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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(())
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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,
|
||||||
|
|
Loading…
Add table
Reference in a new issue