fruits: optimized and tested
This commit is contained in:
@@ -0,0 +1,74 @@
|
||||
use crate::{Beatmap, DifficultyPoint, TimingPoint};
|
||||
|
||||
use std::slice::Iter;
|
||||
|
||||
macro_rules! next_tuple {
|
||||
($iter:expr, ($first:ident, $second:ident)) => {
|
||||
$iter.next().map(|e| (e.$first, e.$second))
|
||||
};
|
||||
}
|
||||
|
||||
pub(crate) struct ControlPointIter<'p> {
|
||||
timing_points: Iter<'p, TimingPoint>,
|
||||
difficulty_points: Iter<'p, DifficultyPoint>,
|
||||
|
||||
next_timing: Option<(f32, f32)>,
|
||||
next_difficulty: Option<(f32, f32)>,
|
||||
}
|
||||
|
||||
impl<'p> ControlPointIter<'p> {
|
||||
#[inline]
|
||||
pub(crate) fn new(map: &'p Beatmap) -> Self {
|
||||
let mut timing_points = map.timing_points.iter();
|
||||
let mut difficulty_points = map.difficulty_points.iter();
|
||||
|
||||
Self {
|
||||
next_timing: next_tuple!(timing_points, (time, beat_len)),
|
||||
next_difficulty: next_tuple!(difficulty_points, (time, speed_multiplier)),
|
||||
|
||||
timing_points,
|
||||
difficulty_points,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) enum ControlPoint {
|
||||
Timing { time: f32, beat_len: f32 },
|
||||
Difficulty { time: f32, speed_mult: f32 },
|
||||
}
|
||||
|
||||
impl ControlPoint {
|
||||
#[inline]
|
||||
pub(crate) fn time(&self) -> f32 {
|
||||
match self {
|
||||
Self::Timing { time, .. } => *time,
|
||||
Self::Difficulty { time, .. } => *time,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'p> Iterator for ControlPointIter<'p> {
|
||||
type Item = ControlPoint;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
match (self.next_timing, self.next_difficulty) {
|
||||
(Some((time, beat_len)), Some((d, _))) if time <= d => {
|
||||
self.next_timing = next_tuple!(self.timing_points, (time, beat_len));
|
||||
|
||||
Some(ControlPoint::Timing { time, beat_len })
|
||||
}
|
||||
(_, Some((time, speed_mult))) => {
|
||||
self.next_difficulty =
|
||||
next_tuple!(self.difficulty_points, (time, speed_multiplier));
|
||||
|
||||
Some(ControlPoint::Difficulty { time, speed_mult })
|
||||
}
|
||||
(Some((time, beat_len)), None) => {
|
||||
self.next_timing = next_tuple!(self.timing_points, (time, beat_len));
|
||||
|
||||
Some(ControlPoint::Timing { time, beat_len })
|
||||
}
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
+117
-66
@@ -1,16 +1,18 @@
|
||||
mod catch_object;
|
||||
mod control_point_iter;
|
||||
mod difficulty_object;
|
||||
mod movement;
|
||||
mod pp;
|
||||
mod slider_state;
|
||||
|
||||
use catch_object::CatchObject;
|
||||
use difficulty_object::DifficultyObject;
|
||||
use movement::Movement;
|
||||
pub use pp::*;
|
||||
use slider_state::SliderState;
|
||||
|
||||
use crate::{curve::Curve, Beatmap, HitObjectKind, Mods, PathType};
|
||||
use crate::{curve::Curve, Beatmap, HitObjectKind, Mods, PathType, Pos2};
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::convert::identity;
|
||||
|
||||
const SECTION_LENGTH: f32 = 750.0;
|
||||
@@ -19,11 +21,7 @@ const STAR_SCALING_FACTOR: f32 = 0.153;
|
||||
const ALLOWED_CATCH_RANGE: f32 = 0.8;
|
||||
const CATCHER_SIZE: f32 = 106.75;
|
||||
|
||||
macro_rules! binary_search {
|
||||
($slice:expr, $target:expr) => {
|
||||
$slice.binary_search_by(|p| p.time.partial_cmp(&$target).unwrap_or(Ordering::Equal))
|
||||
};
|
||||
}
|
||||
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
|
||||
|
||||
/// Star calculation for osu!ctb maps
|
||||
// Slider parsing based on https://github.com/osufx/catch-the-pp
|
||||
@@ -35,9 +33,11 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
let attributes = map.attributes().mods(mods);
|
||||
let with_hr = mods.hr();
|
||||
let mut ticks = Vec::new(); // using the same buffer for all sliders
|
||||
let mut slider_state = SliderState::new(map);
|
||||
|
||||
let mut fruits = 0;
|
||||
let mut droplets = 0;
|
||||
let mut tiny_droplets = 0;
|
||||
|
||||
// BUG: Incorrect object order on 2B maps that have fruits within sliders
|
||||
let mut hit_objects = map
|
||||
@@ -66,48 +66,21 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
.replace(h.pos.x + curve_points[curve_points.len() - 1].x - curve_points[0].x);
|
||||
*last_time = h.start_time;
|
||||
|
||||
let (beat_len, timing_time) = {
|
||||
match binary_search!(map.timing_points, h.start_time) {
|
||||
Ok(idx) => {
|
||||
let point = &map.timing_points[idx];
|
||||
(point.beat_len, point.time)
|
||||
}
|
||||
Err(0) => (1000.0, 0.0),
|
||||
Err(idx) => {
|
||||
let point = &map.timing_points[idx - 1];
|
||||
(point.beat_len, point.time)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let (speed_multiplier, diff_time) = {
|
||||
match binary_search!(map.difficulty_points, h.start_time) {
|
||||
Ok(idx) => {
|
||||
let point = &map.difficulty_points[idx];
|
||||
(point.speed_multiplier, point.time)
|
||||
}
|
||||
Err(0) => (1.0, 0.0),
|
||||
Err(idx) => {
|
||||
let point = &map.difficulty_points[idx - 1];
|
||||
(point.speed_multiplier, point.time)
|
||||
}
|
||||
}
|
||||
};
|
||||
// Responsible for timing point values
|
||||
slider_state.update(h.start_time);
|
||||
|
||||
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
|
||||
|
||||
if map.version >= 8 {
|
||||
tick_distance /= (100.0 / speed_multiplier).max(10.0).min(1000.0) / 100.0;
|
||||
tick_distance /=
|
||||
(100.0 / slider_state.speed_mult).max(10.0).min(1000.0) / 100.0;
|
||||
}
|
||||
|
||||
let spm = if timing_time > diff_time {
|
||||
1.0
|
||||
} else {
|
||||
speed_multiplier
|
||||
};
|
||||
|
||||
let duration = *repeats as f32 * beat_len * *pixel_len / (map.sv * spm) / 100.0;
|
||||
let duration = *repeats as f32 * slider_state.beat_len * pixel_len
|
||||
/ (map.sv * slider_state.speed_mult)
|
||||
/ 100.0;
|
||||
|
||||
// Ensure path type validity
|
||||
let path_type = if *path_type == PathType::PerfectCurve && curve_points.len() > 3 {
|
||||
PathType::Bezier
|
||||
} else if curve_points.len() == 2 {
|
||||
@@ -116,6 +89,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
*path_type
|
||||
};
|
||||
|
||||
// Build the curve w.r.t. the curve points
|
||||
let curve = match path_type {
|
||||
PathType::Linear => Curve::linear(curve_points[0], curve_points[1]),
|
||||
PathType::Bezier => Curve::bezier(curve_points),
|
||||
@@ -129,42 +103,49 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
let target = *pixel_len - tick_distance / 8.0;
|
||||
ticks.reserve((target / tick_distance) as usize);
|
||||
|
||||
while current_distance < target {
|
||||
let pos = curve.point_at_distance(current_distance);
|
||||
// Tick of the first span
|
||||
if current_distance < target {
|
||||
for tick_idx in 1.. {
|
||||
let pos = curve.point_at_distance(current_distance);
|
||||
let time = h.start_time + time_add * tick_idx as f32;
|
||||
ticks.push((pos, time));
|
||||
current_distance += tick_distance;
|
||||
|
||||
ticks.push((pos, h.start_time + time_add * (ticks.len() + 1) as f32));
|
||||
current_distance += tick_distance;
|
||||
if current_distance >= target {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tiny_droplets +=
|
||||
tiny_droplet_count(h.start_time, time_add, duration, *repeats, &ticks);
|
||||
|
||||
let mut slider_objects = Vec::with_capacity(repeats * (ticks.len() + 1));
|
||||
slider_objects.push((h.pos, h.start_time));
|
||||
|
||||
// Other spans
|
||||
if *repeats <= 1 {
|
||||
slider_objects.append(&mut ticks); // automatically empties buffer for next slider
|
||||
} else {
|
||||
slider_objects.append(&mut ticks.clone());
|
||||
|
||||
for repeat_id in 1..*repeats - 1 {
|
||||
for repeat_id in 1..*repeats {
|
||||
let dist = (repeat_id % 2) as f32 * *pixel_len;
|
||||
let time_offset = (duration / *repeats as f32) * repeat_id as f32;
|
||||
let pos = curve.point_at_distance(dist);
|
||||
|
||||
// Reverse tick / last legacy tick
|
||||
// Reverse tick
|
||||
slider_objects.push((pos, h.start_time + time_offset));
|
||||
|
||||
ticks.reverse();
|
||||
slider_objects.extend_from_slice(&ticks); // tick time doesn't need to be adjusted for some reason
|
||||
// Actual ticks
|
||||
if repeat_id & 1 == 1 {
|
||||
slider_objects.extend(ticks.iter().copied().rev());
|
||||
} else {
|
||||
slider_objects.extend(ticks.iter().copied());
|
||||
}
|
||||
}
|
||||
|
||||
// Handling last span separatly so that `ticks` vector isn't cloned again
|
||||
let dist = ((*repeats - 1) % 2) as f32 * *pixel_len;
|
||||
let time_offset = (duration / *repeats as f32) * (*repeats - 1) as f32;
|
||||
let pos = curve.point_at_distance(dist);
|
||||
|
||||
slider_objects.push((pos, h.start_time + time_offset));
|
||||
|
||||
ticks.reverse();
|
||||
slider_objects.append(&mut ticks); // automatically empties buffer for next slider
|
||||
ticks.clear();
|
||||
}
|
||||
|
||||
// Slider tail
|
||||
@@ -280,12 +261,85 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
|
||||
DifficultyAttributes {
|
||||
stars,
|
||||
ar: attributes.ar,
|
||||
n_fruits: fruits,
|
||||
n_droplets: droplets,
|
||||
n_tiny_droplets: tiny_droplets,
|
||||
max_combo: fruits + droplets,
|
||||
}
|
||||
}
|
||||
|
||||
fn tiny_droplet_count(
|
||||
start_time: f32,
|
||||
time_between_ticks: f32,
|
||||
duration: f32,
|
||||
spans: usize,
|
||||
ticks: &[(Pos2, f32)],
|
||||
) -> usize {
|
||||
// tiny droplets preceeding a _tick_
|
||||
let per_tick = if !ticks.is_empty() && time_between_ticks > 80.0 {
|
||||
let time_between_tiny = shrink_down(time_between_ticks);
|
||||
|
||||
// add a little for floating point inaccuracies
|
||||
let start = time_between_tiny + 0.001;
|
||||
|
||||
count_iterations(start, time_between_tiny, time_between_ticks)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// tiny droplets preceeding a _reverse_
|
||||
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
||||
let repeat_time = start_time + duration / spans as f32;
|
||||
let since_last_tick = repeat_time - last;
|
||||
|
||||
let span_last_section = if since_last_tick > 80.0 {
|
||||
let time_between_tiny = shrink_down(since_last_tick);
|
||||
|
||||
count_iterations(time_between_tiny, time_between_tiny, since_last_tick)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// tiny droplets preceeding the slider tail
|
||||
// necessary to handle distinctly because of the legacy last tick
|
||||
let last = ticks.last().map_or(start_time, |(_, last)| *last);
|
||||
let end_time = start_time + duration / spans as f32 - LEGACY_LAST_TICK_OFFSET;
|
||||
let since_last_tick = end_time - last;
|
||||
|
||||
let last_section = if since_last_tick > 80.0 {
|
||||
let time_between_tiny = shrink_down(since_last_tick);
|
||||
|
||||
count_iterations(time_between_tiny, time_between_tiny, since_last_tick)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
// Combine tiny droplets counts
|
||||
per_tick * ticks.len() * spans + span_last_section * (spans.saturating_sub(1)) + last_section
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn shrink_down(mut val: f32) -> f32 {
|
||||
while val > 100.0 {
|
||||
val /= 2.0;
|
||||
}
|
||||
|
||||
val
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn count_iterations(mut start: f32, step: f32, end: f32) -> usize {
|
||||
let mut count = 0;
|
||||
|
||||
while start < end {
|
||||
count += 1;
|
||||
start += step;
|
||||
}
|
||||
|
||||
count
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn calculate_catch_width(cs: f32) -> f32 {
|
||||
let scale = 1.0 - 0.7 * (cs - 5.0) / 5.0;
|
||||
@@ -323,8 +377,10 @@ impl<I: Iterator<Item = CatchObject>> Iterator for FruitOrJuice<I> {
|
||||
pub struct DifficultyAttributes {
|
||||
pub stars: f32,
|
||||
pub max_combo: usize,
|
||||
pub ar: f32,
|
||||
pub n_fruits: usize,
|
||||
pub n_droplets: usize,
|
||||
pub n_tiny_droplets: usize,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@@ -335,22 +391,17 @@ mod tests {
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn fruits_single() {
|
||||
let map_id = 1972149;
|
||||
let file = match File::open(format!("E:/Games/osu!/beatmaps/{}.osu", map_id)) {
|
||||
let file = match File::open("E:/Games/osu!/beatmaps/2206596.osu") {
|
||||
Ok(file) => file,
|
||||
Err(why) => panic!("Could not open file: {}", why),
|
||||
};
|
||||
// let file = match File::open(format!("E:/Games/osu!/beatmaps/{}.osu", map_id)) {
|
||||
// Ok(file) => file,
|
||||
// Err(why) => panic!("Could not open file: {}", why),
|
||||
// };
|
||||
|
||||
let map = match Beatmap::parse(file) {
|
||||
Ok(map) => map,
|
||||
Err(why) => panic!("Error while parsing map: {}", why),
|
||||
};
|
||||
|
||||
let result = PpCalculator::new(&map).mods(256).calculate();
|
||||
let result = PpCalculator::new(&map).mods(64).calculate();
|
||||
|
||||
println!("Stars: {}", result.stars);
|
||||
println!("PP: {}", result.pp);
|
||||
|
||||
+4
-4
@@ -124,7 +124,7 @@ impl<'m> PpCalculator<'m> {
|
||||
.saturating_sub(self.n_misses.saturating_sub(n_droplets))
|
||||
});
|
||||
|
||||
let max_tiny_droplets = 0; // TODO
|
||||
let max_tiny_droplets = attributes.n_tiny_droplets;
|
||||
|
||||
let n_tiny_droplets = self.n_tiny_droplets.unwrap_or_else(|| {
|
||||
((acc * (attributes.max_combo + max_tiny_droplets) as f32).round() as usize)
|
||||
@@ -151,7 +151,7 @@ impl<'m> PpCalculator<'m> {
|
||||
let stars = attributes.stars;
|
||||
|
||||
// Relying heavily on aim
|
||||
let mut pp = (5.0 * ((stars / 0.0049).max(1.0)) - 4.0).powi(2) / 100_000.0;
|
||||
let mut pp = (5.0 * (stars / 0.0049).max(1.0) - 4.0).powi(2) / 100_000.0;
|
||||
|
||||
let mut combo_hits = self.combo_hits();
|
||||
if combo_hits == 0 {
|
||||
@@ -175,7 +175,7 @@ impl<'m> PpCalculator<'m> {
|
||||
}
|
||||
|
||||
// AR scaling
|
||||
let ar = self.map.ar;
|
||||
let ar = attributes.ar;
|
||||
let mut ar_factor = 1.0;
|
||||
if ar > 9.0 {
|
||||
ar_factor += 0.1 * (ar - 9.0) + (ar > 10.0) as u8 as f32 * 0.1 * (ar - 10.0);
|
||||
@@ -231,7 +231,7 @@ impl<'m> PpCalculator<'m> {
|
||||
let total_hits = self.total_hits();
|
||||
|
||||
if total_hits == 0 {
|
||||
0.0
|
||||
1.0
|
||||
} else {
|
||||
(self.successful_hits() as f32 / total_hits as f32)
|
||||
.max(0.0)
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
use super::control_point_iter::{ControlPoint, ControlPointIter};
|
||||
use crate::Beatmap;
|
||||
|
||||
pub(crate) struct SliderState<'p> {
|
||||
control_points: ControlPointIter<'p>,
|
||||
next: Option<ControlPoint>,
|
||||
pub(crate) beat_len: f32,
|
||||
pub(crate) speed_mult: f32,
|
||||
}
|
||||
|
||||
impl<'p> SliderState<'p> {
|
||||
#[inline]
|
||||
pub(crate) fn new(map: &'p Beatmap) -> Self {
|
||||
let mut control_points = ControlPointIter::new(map);
|
||||
|
||||
let (beat_len, speed_mult) = match control_points.next() {
|
||||
Some(ControlPoint::Timing { beat_len, .. }) => (beat_len, 1.0),
|
||||
Some(ControlPoint::Difficulty { speed_mult, .. }) => (1000.0, speed_mult),
|
||||
None => (1000.0, 1.0),
|
||||
};
|
||||
|
||||
Self {
|
||||
next: control_points.next(),
|
||||
control_points,
|
||||
beat_len,
|
||||
speed_mult,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn update(&mut self, time: f32) {
|
||||
while let Some(next) = self.next.as_ref().filter(|n| time >= n.time()) {
|
||||
match next {
|
||||
ControlPoint::Timing { beat_len, .. } => {
|
||||
self.beat_len = *beat_len;
|
||||
self.speed_mult = 1.0;
|
||||
}
|
||||
ControlPoint::Difficulty { speed_mult, .. } => self.speed_mult = *speed_mult,
|
||||
}
|
||||
|
||||
self.next = self.control_points.next();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -6,7 +6,9 @@ pub mod taiko;
|
||||
|
||||
mod curve;
|
||||
mod math_util;
|
||||
mod mods;
|
||||
|
||||
pub use mods::Mods;
|
||||
pub use parse::*;
|
||||
|
||||
#[inline]
|
||||
|
||||
@@ -49,7 +49,7 @@ pub fn stars(map: &Beatmap, mods: impl Mods) -> DifficultyAttributes {
|
||||
scaling_factor *= 1.0 + small_circle_bonus;
|
||||
}
|
||||
|
||||
let mut slider_state = SliderState::new(&map);
|
||||
let mut slider_state = SliderState::new(map);
|
||||
let mut ticks_buf = Vec::new();
|
||||
|
||||
let mut hit_objects = map.hit_objects.iter().map(|h| {
|
||||
@@ -147,6 +147,7 @@ mod tests {
|
||||
use std::fs::File;
|
||||
|
||||
#[test]
|
||||
#[ignore]
|
||||
fn no_leniency_single_stars() {
|
||||
let file = match File::open("./test/70090.osu") {
|
||||
Ok(file) => file,
|
||||
|
||||
@@ -45,7 +45,9 @@ impl OsuObject {
|
||||
let mut end_pos = h.pos;
|
||||
let mut travel_dist = 0.0;
|
||||
|
||||
slider_state.update(h.start_time); // Responsible for timing point values
|
||||
// Responsible for timing point values
|
||||
slider_state.update(h.start_time);
|
||||
|
||||
let approx_follow_circle_radius = radius * 3.0;
|
||||
let mut tick_distance = 100.0 * map.sv / map.tick_rate;
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::Mods;
|
||||
use crate::Mods;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct BeatmapAttributes {
|
||||
|
||||
+1
-3
@@ -3,7 +3,6 @@ mod control_point;
|
||||
mod error;
|
||||
mod hitobject;
|
||||
mod hitsound;
|
||||
mod mods;
|
||||
mod pos2;
|
||||
mod sort;
|
||||
|
||||
@@ -12,7 +11,6 @@ pub use control_point::{DifficultyPoint, TimingPoint};
|
||||
pub use error::{ParseError, ParseResult};
|
||||
pub use hitobject::{HitObject, HitObjectKind};
|
||||
pub use hitsound::HitSound;
|
||||
pub use mods::Mods;
|
||||
pub use pos2::Pos2;
|
||||
use sort::sort;
|
||||
|
||||
@@ -407,7 +405,7 @@ mod tests {
|
||||
use std::fs::File;
|
||||
|
||||
#[test]
|
||||
fn parsing_works() {
|
||||
fn parsing() {
|
||||
let file = match File::open("E:/Games/osu!/beatmaps/2223745.osu") {
|
||||
Ok(file) => file,
|
||||
Err(why) => panic!("Could not read file: {}", why),
|
||||
|
||||
+25
-25
@@ -13,7 +13,7 @@ struct MapResult {
|
||||
#[test]
|
||||
fn fruits() {
|
||||
let star_margin = 0.005;
|
||||
let pp_margin = 0.005;
|
||||
let pp_margin = 0.015;
|
||||
|
||||
for result in RESULTS {
|
||||
let MapResult {
|
||||
@@ -64,147 +64,147 @@ const RESULTS: &[MapResult] = &[
|
||||
map_id: 1977380,
|
||||
mods: 256,
|
||||
stars: 2.0564713386286573,
|
||||
pp: 43.49758286973066,
|
||||
pp: 45.1590377326849,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 1977380,
|
||||
mods: 0,
|
||||
stars: 2.5695489769068742,
|
||||
pp: 71.54271564752817,
|
||||
pp: 67.73537806280385,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 1977380,
|
||||
mods: 8,
|
||||
stars: 2.5695489769068742,
|
||||
pp: 78.29197525261374,
|
||||
pp: 81.28245367536461,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 1977380,
|
||||
mods: 64,
|
||||
stars: 3.589887228221038,
|
||||
pp: 135.95326950246636,
|
||||
pp: 141.14620680718699,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 1977380,
|
||||
mods: 16,
|
||||
stars: 3.1515873669521928,
|
||||
pp: 108.02360048697571,
|
||||
pp: 112.14972254944568,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 1977380,
|
||||
mods: 2,
|
||||
stars: 3.0035260129778396,
|
||||
pp: 98.10009237095251,
|
||||
pp: 101.84717128368449,
|
||||
},
|
||||
// -----
|
||||
MapResult {
|
||||
map_id: 1974968,
|
||||
mods: 256,
|
||||
stars: 1.9544305373156605,
|
||||
pp: 40.46051204584743,
|
||||
pp: 42.91338693937752,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 1974968,
|
||||
mods: 0,
|
||||
stars: 2.521701539665241,
|
||||
pp: 64.28153872477789,
|
||||
pp: 68.1785376623302,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 1974968,
|
||||
mods: 8,
|
||||
stars: 2.521701539665241,
|
||||
pp: 81.9589618740918,
|
||||
pp: 86.927635519471,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 1974968,
|
||||
mods: 64,
|
||||
stars: 3.650649037957456,
|
||||
pp: 131.5628579590708,
|
||||
pp: 139.53871429136828,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 1974968,
|
||||
mods: 16,
|
||||
stars: 3.566302788963401,
|
||||
pp: 135.59111737415918,
|
||||
pp: 143.81118258776544,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 1974968,
|
||||
mods: 2,
|
||||
stars: 2.2029392066882654,
|
||||
pp: 53.2211645832911,
|
||||
pp: 56.44764027057014,
|
||||
},
|
||||
// -----
|
||||
MapResult {
|
||||
map_id: 2420076,
|
||||
mods: 256,
|
||||
stars: 4.791039358886245,
|
||||
pp: 226.85533170425614,
|
||||
pp: 258.46694642171224,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 2420076,
|
||||
mods: 0,
|
||||
stars: 6.223136555625056,
|
||||
pp: 413.51912544400295,
|
||||
pp: 471.1417837859138,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 2420076,
|
||||
mods: 8,
|
||||
stars: 6.223136555625056,
|
||||
pp: 440.3978626824246,
|
||||
pp: 501.7659929922609,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 2420076,
|
||||
mods: 64,
|
||||
stars: 8.908315960310958,
|
||||
pp: 999.4280253427237,
|
||||
pp: 1138.695343583009,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 2420076,
|
||||
mods: 16,
|
||||
stars: 6.54788067620051,
|
||||
pp: 466.3097817709075,
|
||||
pp: 531.2886608194283,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 2420076,
|
||||
mods: 2,
|
||||
stars: 6.067971540209479,
|
||||
pp: 392.2324532647843,
|
||||
pp: 446.888877247154,
|
||||
},
|
||||
// -----
|
||||
MapResult {
|
||||
map_id: 2206596,
|
||||
mods: 256,
|
||||
stars: 4.767182611189798,
|
||||
pp: 227.40643918013868,
|
||||
pp: 300.15942914986067,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 2206596,
|
||||
mods: 0,
|
||||
stars: 6.157660207091584,
|
||||
pp: 402.3258172661857,
|
||||
pp: 531.0398776668264,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 2206596,
|
||||
mods: 8,
|
||||
stars: 6.157660207091584,
|
||||
pp: 434.5118711368466,
|
||||
pp: 573.5230526869998,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 2206596,
|
||||
mods: 64,
|
||||
stars: 8.93391286552717,
|
||||
pp: 996.4288537655079,
|
||||
pp: 1315.2112887084272,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 2206596,
|
||||
mods: 16,
|
||||
stars: 6.8639096665110735,
|
||||
pp: 518.8398368985938,
|
||||
pp: 684.8296373011866,
|
||||
},
|
||||
MapResult {
|
||||
map_id: 2206596,
|
||||
mods: 2,
|
||||
stars: 5.60279198088948,
|
||||
pp: 339.327091261929,
|
||||
pp: 447.8862884246722,
|
||||
},
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user