big first commit

This commit is contained in:
MaxOhn
2021-01-08 09:25:46 +01:00
commit 496216843f
61 changed files with 32981 additions and 0 deletions
+2
View File
@@ -0,0 +1,2 @@
/target
Cargo.lock
+10
View File
@@ -0,0 +1,10 @@
[package]
name = "osu"
version = "0.1.0"
authors = ["MaxOhn <ohn.m@hotmail.de>"]
edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies.parse]
path = "../parse"
+149
View File
@@ -0,0 +1,149 @@
use super::math_util;
use parse::Pos2;
const SLIDER_QUALITY: f32 = 50.0;
pub(crate) enum Points {
Single(Pos2),
Multi(Vec<Pos2>),
}
pub(crate) enum Curve {
Linear {
a: Pos2,
b: Pos2,
},
Bezier(Points),
Catmull(Points),
Perfect {
origin: Pos2,
cx: f32,
cy: f32,
radius: f32,
},
}
impl Curve {
pub(crate) fn linear(a: Pos2, b: Pos2) -> Self {
Self::Linear { a, b }
}
pub(crate) fn bezier(points: &[Pos2]) -> Self {
if points.len() == 1 {
return Self::Bezier(Points::Single(points[0]));
}
let mut start = 0;
let mut end = 0;
let mut result = Vec::with_capacity(4);
for i in 0..points.len() - 1 {
if end - start > 1 && points[i] == points[end - 1] {
Self::_bezier(&mut result, &points[start..end]);
start = end;
}
end += 1;
}
Self::_bezier(&mut result, &points[start..end + 1]);
Self::Bezier(Points::Multi(result))
}
fn _bezier(result: &mut Vec<Pos2>, points: &[Pos2]) {
let step = 0.25 / SLIDER_QUALITY / points.len() as f32;
let mut i = 0.0;
let n = points.len() as i32 - 1;
while i < 1.0 + step {
let point = (0..=n).fold(Pos2 { x: 0.0, y: 0.0 }, |point, p| {
let factor = math_util::cpn(p, n) * (1.0 - i).powi(n - p) * i.powi(p);
point + points[p as usize] * factor
});
result.push(point);
i += step;
}
}
pub(crate) fn catmull(points: &[Pos2]) -> Self {
if points.len() == 1 {
return Self::Catmull(Points::Single(points[0]));
}
let order = points.len();
let step = 2.5 / SLIDER_QUALITY;
let target = step + 1.0;
let mut resulting_points = Vec::with_capacity(4);
for x in 0..order - 1 {
let mut t = 0.0;
while t < target {
let v1 = if x >= 1 { points[x - 1] } else { points[x] };
let v2 = points[x];
let v3 = if x + 1 < order {
points[x + 1]
} else {
v2.add_scaled(v2.add_scaled(v1, -1.0), 1.0)
};
let v4 = if x + 2 < order {
points[x + 2]
} else {
v3.add_scaled(v3.add_scaled(v2, -1.0), 1.0)
};
let point = Self::catmull_point(v1, v2, v3, v4, t);
resulting_points.push(point);
t += step;
}
}
Self::Catmull(Points::Multi(resulting_points))
}
#[inline]
fn catmull_point(p0: Pos2, p1: Pos2, p2: Pos2, p3: Pos2, len: f32) -> Pos2 {
Pos2 {
x: math_util::catmull(p0.x, p1.x, p2.x, p3.x, len),
y: math_util::catmull(p0.y, p1.y, p2.y, p3.y, len),
}
}
pub(crate) fn perfect(points: &[Pos2]) -> Self {
let (cx, cy, mut radius) = math_util::get_circum_circle(&points);
radius *= ((!math_util::is_left(&points)) as i8 * 2 - 1) as f32;
Self::Perfect {
origin: points[0],
cx,
cy,
radius,
}
}
pub(crate) fn point_at_distance(&self, len: f32) -> Pos2 {
let points = match self {
Self::Bezier(points) => points,
Self::Catmull(points) => points,
Self::Linear { a, b } => return math_util::point_on_line(*a, *b, len),
Self::Perfect {
origin,
cx,
cy,
radius,
} => return math_util::rotate(*cx, *cy, *origin, len / *radius),
};
match points {
Points::Single(point) => *point,
Points::Multi(points) => math_util::point_at_distance(points, len),
}
}
}
+70
View File
@@ -0,0 +1,70 @@
use super::OsuObject;
const NORMALIZED_RADIUS: f32 = 52.0;
pub(crate) struct DifficultyObject {
pub(crate) base: OsuObject,
pub(crate) prev: Option<(f32, f32)>, // (jump_dist, strain_time)
pub(crate) jump_dist: f32,
pub(crate) travel_dist: f32,
pub(crate) angle: Option<f32>,
pub(crate) delta: f32,
pub(crate) strain_time: f32,
}
impl DifficultyObject {
pub(crate) fn new(
base: OsuObject,
prev: OsuObject,
prev_diff: Option<DifficultyObject>,
prev_prev: Option<OsuObject>,
clock_rate: f32,
) -> Self {
let delta = (base.time() - prev.time()) / clock_rate;
let strain_time = delta.max(50.0);
let radius = base.radius();
let mut scaling_factor = NORMALIZED_RADIUS / radius;
if radius < 30.0 {
let small_circle_bonus = (30.0 - radius).min(5.0) / 50.0;
scaling_factor *= 1.0 + small_circle_bonus;
}
let travel_dist = base.travel_dist();
let prev_cursor_pos = prev.cursor_end_position();
let jump_dist = match base {
OsuObject::Spinner { .. } => 0.0,
_ => (base.stacked_pos() * scaling_factor - prev_cursor_pos * scaling_factor).length(),
};
let angle = prev_prev.map(|prev_prev| {
let prev_prev_cursor_pos = prev_prev.cursor_end_position();
let v1 = prev_prev_cursor_pos - prev.stacked_pos();
let v2 = base.stacked_pos() - prev_cursor_pos;
let dot = v1.dot(v2);
let det = v1.x * v2.y - v1.y * v2.x;
det.atan2(dot).abs()
});
let prev = prev_diff.map(|o| (o.jump_dist, o.strain_time));
Self {
base,
prev,
jump_dist,
travel_dist,
angle,
delta,
strain_time,
}
}
}
+166
View File
@@ -0,0 +1,166 @@
mod curve;
mod difficulty_object;
mod math_util;
mod osu_object;
mod skill;
mod skill_kind;
use difficulty_object::DifficultyObject;
use osu_object::OsuObject;
use skill::Skill;
use skill_kind::SkillKind;
use parse::{Beatmap, Mods};
const SECTION_LEN: f32 = 400.0;
const DIFFICULTY_MULTIPLIER: f32 = 0.0675;
/// Star calculation for osu!standard maps
pub fn stars(map: &Beatmap, mods: impl Mods) -> f32 {
if map.hit_objects.len() < 2 {
return 0.0;
}
let attributes = map.attributes().mods(mods);
let section_len = SECTION_LEN * attributes.clock_rate;
let mut hit_objects = map
.hit_objects
.iter()
.map(|h| OsuObject::new(h, map, &attributes));
let mut skills = vec![Skill::new(SkillKind::Aim), Skill::new(SkillKind::Speed)];
let mut current_section_end =
(map.hit_objects[0].start_time / section_len).ceil() * section_len;
let mut prev_prev = None;
let mut prev = hit_objects.next().unwrap();
let mut prev_diff = None;
for curr in hit_objects {
let h = DifficultyObject::new(
curr.clone(),
prev.clone(),
prev_diff,
prev_prev,
attributes.clock_rate,
);
// println!(
// "strain_time={} | travel_dist={} | jump_dist={} | angle={:?}",
// h.strain_time, h.travel_dist, h.jump_dist, h.angle
// );
while h.base.time() > current_section_end {
for skill in skills.iter_mut() {
skill.save_current_peak();
skill.start_new_section_from(current_section_end);
}
current_section_end += section_len;
}
for skill in skills.iter_mut() {
skill.process(&h);
}
prev_prev = Some(prev);
prev = curr;
prev_diff = Some(h);
}
for skill in skills.iter_mut() {
skill.save_current_peak();
}
let aim_rating = skills[0].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
let speed_rating = skills[1].difficulty_value().sqrt() * DIFFICULTY_MULTIPLIER;
aim_rating + speed_rating + (aim_rating - speed_rating).abs() / 2.0
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs::File;
#[test]
fn test_single() {
// let file = match File::open("E:/Games/osu!/beatmaps/1851299.osu") {
// Ok(file) => file,
// Err(why) => panic!("Could not open file: {}", why),
// };
let file = match File::open("C:/Users/Max/Desktop/1851299.osu") {
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 stars = stars(&map, 0);
println!("Stars: {}", stars);
}
#[test]
#[ignore]
fn test_osu() {
let margin = 0.005;
#[rustfmt::skip]
let data = vec![
(1851299, 1 << 8, 4.23514130038547), // HT
(1851299, 0, 5.356786475158158), // NM
(1851299, 1 << 6, 7.450616908751305), // DT
(1851299, 1 << 4, 5.6834681957637665),// HR
(1851299, 1 << 1, 4.937817303399699), // EZ
(70090, 1 << 8, 2.2929922580201803), // HT
(70090, 0, 2.8322940761833983), // NM
(70090, 1 << 6, 3.8338563325375485), // DT
(70090, 1 << 4, 3.0617492228478174), // HR
(70090, 1 << 1, 2.698823231324141), // EZ
(1241370, 1 << 8, 5.662809600985943), // HT
(1241370, 0, 7.0367002127481975), // NM
(1241370, 1 << 6, 11.144720506574934),// DT
(1241370, 1 << 4, 7.641688110458715), // HR
(1241370, 1 << 1, 6.316288616688052), // EZ
// Slider fiesta
// (1657535, 1 << 8, 4.1727975286379895),// HT
// (1657535, 0, 5.16048239944917), // NM
// (1657535, 1 << 6, 7.125936779100417), // DT
// (1657535, 1 << 4, 5.545877027713307), // HR
// (1657535, 1 << 1, 4.66015083361088), // EZ
];
for (map_id, mods, expected_stars) in data {
let file = match File::open(format!("./test/{}.osu", map_id)) {
Ok(file) => file,
Err(why) => panic!("Could not open file {}.osu: {}", map_id, why),
};
let map = match Beatmap::parse(file) {
Ok(map) => map,
Err(why) => panic!("Error while parsing map {}: {}", map_id, why),
};
let stars = stars(&map, mods);
assert!(
(stars - expected_stars).abs() < margin,
"Stars: {} | Expected: {} => {} margin [map {} | mods {}]",
stars,
expected_stars,
(stars - expected_stars).abs(),
map_id,
mods
);
}
}
}
+133
View File
@@ -0,0 +1,133 @@
use parse::Pos2;
#[inline]
pub(crate) fn cpn(mut p: i32, n: i32) -> f32 {
if p < 0 || p > n {
return 0.0;
}
p = p.min(n - p);
let mut out = 1.0;
for i in 1..=p {
out *= (n - p + i) as f32 / i as f32;
}
out
}
#[inline]
pub(crate) fn catmull(p0: f32, p1: f32, p2: f32, p3: f32, t: f32) -> f32 {
0.5 * ((2.0 * p1)
+ (-p0 + p2) * t
+ (2.0 * p0 - 5.0 * p1 + 4.0 * p2 - p3) * t.powi(2)
+ (-p0 + 3.0 * p1 - 3.0 * p2 + p3 * t.powi(3)))
}
#[inline]
pub(crate) fn point_on_line(p1: Pos2, p2: Pos2, len: f32) -> Pos2 {
let mut full_len = ((p1.x - p2.x).powi(2) + (p1.y - p2.y).powi(2)).sqrt();
let n = full_len - len;
if full_len.abs() < f32::EPSILON {
full_len = 1.0;
}
(p1 * n + p2 * len) / full_len
}
#[inline]
pub(crate) fn angle_from_points(p0: Pos2, p1: Pos2) -> f32 {
(p1.y - p0.y).atan2(p1.x - p0.x)
}
#[inline]
pub(crate) fn distance_from_points(arr: &[Pos2]) -> f32 {
arr.iter()
.skip(1)
.zip(arr.iter())
.map(|(curr, prev)| curr.distance(prev))
.sum()
}
#[inline]
pub(crate) fn cart_from_pol(r: f32, t: f32) -> Pos2 {
Pos2 {
x: r * t.cos(),
y: r * t.sin(),
}
}
pub(crate) fn point_at_distance(array: &[Pos2], distance: f32) -> Pos2 {
if array.len() < 2 {
return Pos2 { x: 0.0, y: 0.0 };
} else if distance.abs() < f32::EPSILON {
return array[0];
} else if distance_from_points(array) <= distance {
return array[array.len() - 1];
}
let mut i = 0;
let mut current_distance = 0.0;
let mut new_distance = 0.0;
while i < array.len() - 2 {
new_distance = (array[i] - array[i + 1]).length();
current_distance += new_distance;
if distance <= current_distance {
break;
}
i += 1;
}
current_distance -= new_distance;
if (distance - current_distance).abs() <= f32::EPSILON {
array[i]
} else {
let angle = angle_from_points(array[i], array[i + 1]);
let cart = cart_from_pol(distance - current_distance, angle);
if array[i].x > array[i + 1].x {
array[i] - cart
} else {
array[i] + cart
}
}
}
pub(crate) fn get_circum_circle(p: &[Pos2]) -> (f32, f32, f32) {
let d = 2.0
* (p[0].x * (p[1].y - p[2].y) + p[1].x * (p[2].y - p[0].y) + p[2].x * (p[0].y - p[1].y));
let p0 = p[0].x * p[0].x + p[0].y * p[0].y;
let p1 = p[1].x * p[1].x + p[1].y * p[1].y;
let p2 = p[2].x * p[2].x + p[2].y * p[2].y;
let ux = (p0 * (p[1].y - p[2].y) + p1 * (p[2].y - p[0].y) + p2 * (p[0].y - p[1].y)) / d;
let uy = (p0 * (p[2].x - p[1].x) + p1 * (p[0].x - p[2].x) + p2 * (p[1].x - p[0].x)) / d;
let px = ux - p[0].x;
let py = uy - p[0].y;
let r = (px * px + py * py).sqrt();
(ux, uy, r)
}
#[inline]
pub(crate) fn is_left(p: &[Pos2]) -> bool {
((p[1].x - p[0].x) * (p[2].y - p[0].y) - (p[1].y - p[0].y) * (p[2].x - p[0].x)) < 0.0
}
#[inline]
pub(crate) fn rotate(cx: f32, cy: f32, p: Pos2, radians: f32) -> Pos2 {
let cos = radians.cos();
let sin = radians.sin();
Pos2 {
x: (cos * (p.x - cx)) - (sin * (p.y - cy)) + cx,
y: (sin * (p.x - cx)) + (cos * (p.y - cy)) + cy,
}
}
+324
View File
@@ -0,0 +1,324 @@
use super::curve::Curve;
use parse::{Beatmap, BeatmapAttributes, HitObject, HitObjectKind, PathType, Pos2};
use std::cmp::Ordering;
macro_rules! binary_search {
($slice:expr, $target:expr) => {
$slice.binary_search_by(|p| p.time.partial_cmp(&$target).unwrap_or(Ordering::Equal))
};
}
const OBJECT_RADIUS: f32 = 64.0;
const STACK_DIST: f32 = 3.0;
const LEGACY_LAST_TICK_OFFSET: f32 = 36.0;
#[derive(Clone)] // TODO: Remove clone
pub(crate) enum OsuObject {
Circle {
pos: Pos2,
time: f32,
scale: f32,
stack_height: f32,
},
Slider {
pixel_len: f32, // TODO: Redundant?
repeats: usize, // TODO: Redundant?
objects: Vec<SliderTick>,
scale: f32,
stack_height: f32,
cursor_end_pos: Pos2,
cursor_travel_dist: f32,
},
Spinner {
pos: Pos2,
time: f32,
},
}
impl OsuObject {
pub(crate) fn new(h: &HitObject, map: &Beatmap, attributes: &BeatmapAttributes) -> Self {
let pos = h.pos;
let time = h.start_time;
let scale = (1.0 - 0.7 * (attributes.cs - 5.0) / 5.0) / 2.0;
let mut stack_height = 0.0;
match &h.kind {
HitObjectKind::Circle => Self::Circle {
pos,
time,
scale,
stack_height,
},
HitObjectKind::Slider {
pixel_len,
repeats,
curve_points,
path_type,
} => {
let (beat_len, timing_time) = {
match binary_search!(map.timing_points, 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, 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)
}
}
};
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;
}
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 velocity = *pixel_len as f32 / duration;
// println!("duration={}", duration);
// println!("velocity={}", velocity);
let path_type = if *path_type == PathType::PerfectCurve && curve_points.len() > 3 {
PathType::Bezier
} else if curve_points.len() == 2 {
PathType::Linear
} else {
*path_type
};
let curve = match path_type {
PathType::Linear => Curve::linear(curve_points[0], curve_points[1]),
PathType::Bezier => Curve::bezier(&curve_points),
PathType::Catmull => Curve::catmull(&curve_points),
PathType::PerfectCurve => Curve::perfect(&curve_points),
};
let mut current_distance = tick_distance;
let time_add = duration * (tick_distance / (pixel_len * *repeats as f32));
let target = pixel_len - tick_distance / 8.0;
let mut ticks = Vec::with_capacity((target / tick_distance) as usize);
while current_distance < target {
let pos = curve.point_at_distance(current_distance);
let time = h.start_time + time_add * (ticks.len() + 1) as f32;
ticks.push(SliderTick::new(pos, time));
current_distance += tick_distance;
}
let mut slider_objects = Vec::with_capacity(repeats * (ticks.len() + 1));
slider_objects.push(SliderTick::new(h.pos, h.start_time));
if *repeats <= 1 {
slider_objects.append(&mut ticks);
} else {
slider_objects.append(&mut ticks.clone());
for repeat_id in 1..repeats - 1 {
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
slider_objects.push(SliderTick::new(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
}
// 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(SliderTick::new(pos, h.start_time + time_offset));
ticks.reverse();
slider_objects.append(&mut ticks);
}
// Slider tail
let dist_end = (repeats % 2) as f32 * pixel_len;
// let dist_end = velocity * (duration - LEGACY_LAST_TICK_OFFSET);
let pos = curve.point_at_distance(dist_end);
slider_objects.push(SliderTick::new(pos, h.start_time + duration));
println!("> Slider: {:?}", slider_objects);
let radius = OBJECT_RADIUS * scale;
let stack_offset = {
let c = stack_height * scale * -6.4;
Pos2 { x: c, y: c }
};
// println!("radius={} | stack_offset={:?}", radius, stack_offset);
let stacked_pos = pos + stack_offset;
let mut cursor_end_pos = stacked_pos;
let mut cursor_travel_dist = 0.0;
let approx_follow_circle_radius = radius * 3.0;
for (i, tick) in slider_objects.iter().skip(1).enumerate() {
let diff = stacked_pos + tick.pos - cursor_end_pos;
let mut dist = diff.length();
println!("[{}] diff={:?} | dist={}", i, diff, dist);
if dist > approx_follow_circle_radius {
let normalized = diff.normalize();
dist -= approx_follow_circle_radius;
cursor_end_pos += normalized * dist;
cursor_travel_dist += dist;
}
}
println!("---");
Self::Slider {
pixel_len: *pixel_len,
repeats: *repeats,
objects: slider_objects,
scale,
stack_height,
cursor_end_pos,
cursor_travel_dist,
}
}
HitObjectKind::Spinner { .. } => Self::Spinner { pos, time },
HitObjectKind::Hold { .. } => panic!("found Hold object in osu!standard file"),
}
}
#[inline]
pub(crate) fn time(&self) -> f32 {
match self {
Self::Circle { time, .. } => *time,
Self::Slider { objects, .. } => objects[0].time,
Self::Spinner { time, .. } => *time,
}
}
#[inline]
pub(crate) fn radius(&self) -> f32 {
OBJECT_RADIUS * self.scale()
}
#[inline]
pub(crate) fn travel_dist(&self) -> f32 {
match self {
Self::Slider {
cursor_travel_dist, ..
} => *cursor_travel_dist,
_ => 0.0,
}
}
#[inline]
pub(crate) fn stacked_pos(&self) -> Pos2 {
self.pos() + self.stack_offset()
}
#[inline]
pub(crate) fn cursor_end_position(&self) -> Pos2 {
match self {
Self::Circle { pos, .. } => *pos,
Self::Slider { cursor_end_pos, .. } => *cursor_end_pos,
Self::Spinner { pos, .. } => *pos,
}
}
#[inline]
pub(crate) fn is_spinner(&self) -> bool {
matches!(self, Self::Spinner { .. })
}
#[inline]
fn scale(&self) -> f32 {
match self {
Self::Circle { scale, .. } => *scale,
Self::Slider { scale, .. } => *scale,
Self::Spinner { .. } => 1.0,
}
}
#[inline]
fn stack_height(&self) -> f32 {
match self {
Self::Circle { stack_height, .. } => *stack_height,
Self::Slider { stack_height, .. } => *stack_height,
Self::Spinner { .. } => 0.0,
}
}
#[inline]
fn pos(&self) -> Pos2 {
match self {
Self::Circle { pos, .. } => *pos,
Self::Slider { objects, .. } => objects[0].pos,
Self::Spinner { .. } => Pos2::default(),
}
}
#[inline]
fn stack_offset(&self) -> Pos2 {
let c = self.stack_height() * self.scale() * -6.4;
Pos2 { x: c, y: c }
}
}
#[derive(Copy, Clone)]
pub(crate) struct SliderTick {
pos: Pos2,
time: f32,
}
impl SliderTick {
fn new(pos: Pos2, time: f32) -> Self {
Self { pos, time }
}
}
use std::fmt;
impl fmt::Debug for SliderTick {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{{pos={:?} | time={}}}", self.pos, self.time)
}
}
+99
View File
@@ -0,0 +1,99 @@
use super::{DifficultyObject, SkillKind};
use std::cmp::Ordering;
const SPEED_SKILL_MULTIPLIER: f32 = 1400.0;
const SPEED_STRAIN_DECAY_BASE: f32 = 0.3;
const AIM_SKILL_MULTIPLIER: f32 = 26.25;
const AIM_STRAIN_DECAY_BASE: f32 = 0.15;
const DECAY_WEIGHT: f32 = 0.9;
pub(crate) struct Skill {
pub current_strain: f32,
current_section_peak: f32,
kind: SkillKind,
strain_peaks: Vec<f32>,
prev_time: Option<f32>,
}
impl Skill {
#[inline]
pub(crate) fn new(kind: SkillKind) -> Self {
Self {
current_strain: 1.0,
current_section_peak: 1.0,
kind,
strain_peaks: Vec::with_capacity(128),
prev_time: None,
}
}
#[inline]
pub(crate) fn save_current_peak(&mut self) {
if self.prev_time.is_some() {
self.strain_peaks.push(self.current_section_peak);
}
}
#[inline]
pub(crate) fn start_new_section_from(&mut self, time: f32) {
if let Some(prev) = self.prev_time {
self.current_section_peak = self.peak_strain(time - prev);
}
}
#[inline]
pub(crate) fn process(&mut self, current: &DifficultyObject) {
self.current_strain *= self.strain_decay(current.delta);
self.current_strain += self.kind.strain_value_of(&current) * self.skill_multiplier();
self.current_section_peak = self.current_section_peak.max(self.current_strain);
self.prev_time.replace(current.base.time());
}
pub(crate) fn difficulty_value(&mut self) -> f32 {
let mut difficulty = 0.0;
let mut weight = 1.0;
self.strain_peaks
.sort_unstable_by(|a, b| b.partial_cmp(a).unwrap_or(Ordering::Equal));
for &strain in self.strain_peaks.iter() {
difficulty += strain * weight;
weight *= DECAY_WEIGHT;
}
difficulty
}
#[inline]
fn skill_multiplier(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_SKILL_MULTIPLIER,
SkillKind::Speed => SPEED_SKILL_MULTIPLIER,
}
}
#[inline]
fn strain_decay_base(&self) -> f32 {
match self.kind {
SkillKind::Aim => AIM_STRAIN_DECAY_BASE,
SkillKind::Speed => SPEED_STRAIN_DECAY_BASE,
}
}
#[inline]
fn peak_strain(&self, delta_time: f32) -> f32 {
self.current_strain * self.strain_decay(delta_time)
}
#[inline]
fn strain_decay(&self, ms: f32) -> f32 {
self.strain_decay_base().powf(ms / 1000.0)
}
}
+101
View File
@@ -0,0 +1,101 @@
use super::DifficultyObject;
const SINGLE_SPACING_TRESHOLD: f32 = 125.0;
const SPEED_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_6;
const PI_OVER_4: f32 = std::f32::consts::FRAC_PI_4;
const PI_OVER_2: f32 = std::f32::consts::FRAC_PI_2;
const MIN_SPEED_BONUS: f32 = 75.0;
const MAX_SPEED_BONUS: f32 = 45.0;
const SPEED_BALANCING_FACTOR: f32 = 40.0;
const AIM_ANGLE_BONUS_BEGIN: f32 = std::f32::consts::FRAC_PI_3;
const TIMING_THRESHOLD: f32 = 107.0;
#[derive(Copy, Clone)]
pub(crate) enum SkillKind {
Aim,
Speed,
}
impl SkillKind {
pub(crate) fn strain_value_of(self, current: &DifficultyObject) -> f32 {
match self {
Self::Aim => {
if current.base.is_spinner() {
return 0.0;
}
let mut result = 0.0;
if let Some((prev_jump_dist, prev_strain_time)) = current.prev {
if let Some(angle) = current.angle.filter(|a| *a > AIM_ANGLE_BONUS_BEGIN) {
let scale = 90.0;
let angle_bonus = (((angle - AIM_ANGLE_BONUS_BEGIN).sin()).powi(2)
* (prev_jump_dist - scale).max(0.0)
* (current.jump_dist - scale).max(0.0))
.sqrt();
result = 1.5 * apply_diminishing_exp(angle_bonus.max(0.0))
/ (TIMING_THRESHOLD).max(prev_strain_time)
}
}
let jump_dist_exp = apply_diminishing_exp(current.jump_dist);
let travel_dist_exp = apply_diminishing_exp(current.travel_dist);
let dist_exp =
jump_dist_exp + travel_dist_exp + (travel_dist_exp * jump_dist_exp).sqrt();
(result + dist_exp / (current.strain_time).max(TIMING_THRESHOLD))
.max(dist_exp / current.strain_time)
}
Self::Speed => {
if current.base.is_spinner() {
return 0.0;
}
let dist = SINGLE_SPACING_TRESHOLD.min(current.travel_dist + current.jump_dist);
let delta_time = MAX_SPEED_BONUS.max(current.delta);
let mut speed_bonus = 1.0;
if delta_time < MIN_SPEED_BONUS {
let exp_base = (MIN_SPEED_BONUS - delta_time) / SPEED_BALANCING_FACTOR;
speed_bonus = 1.0 + exp_base * exp_base;
}
let mut angle_bonus = 1.0;
if let Some(angle) = current.angle.filter(|a| *a < SPEED_ANGLE_BONUS_BEGIN) {
let exp_base = (1.5 * (SPEED_ANGLE_BONUS_BEGIN - angle)).sin();
angle_bonus = 1.0 + exp_base * exp_base / 3.57;
if angle < PI_OVER_2 {
angle_bonus = 1.28;
// TODO: Improve ifs
if dist < 90.0 && angle < PI_OVER_4 {
angle_bonus += (1.0 - angle_bonus) * ((90.0 - dist) / 10.0).min(1.0);
} else if dist < 90.0 {
angle_bonus += (1.0 - angle_bonus)
* ((90.0 - dist) / 10.0).min(1.0)
* ((PI_OVER_2 - angle) / PI_OVER_4).sin();
}
}
}
(1.0 + (speed_bonus - 1.0) * 0.75)
* angle_bonus
* (0.95 + speed_bonus * (dist / SINGLE_SPACING_TRESHOLD).powf(3.5))
/ current.strain_time
}
}
}
}
#[inline]
fn apply_diminishing_exp(val: f32) -> f32 {
val.powf(0.99)
}
+2396
View File
File diff suppressed because it is too large Load Diff
+1263
View File
File diff suppressed because it is too large Load Diff
+188
View File
@@ -0,0 +1,188 @@
osu file format v14
[General]
AudioFilename: audio.mp3
AudioLeadIn: 0
PreviewTime: 2154
Countdown: 0
SampleSet: Soft
StackLeniency: 0.7
Mode: 0
LetterboxInBreaks: 0
WidescreenStoryboard: 1
[Editor]
Bookmarks: 14272,22742,24684,25566,28390,34037
DistanceSpacing: 0.8
BeatDivisor: 3
GridSize: 4
TimelineZoom: 1.3
[Metadata]
Title:Harumachi Clover (Swing Arrangement)
TitleUnicode:春待ちクローバー (Swing Arrangement)
Artist:Will Stetson
ArtistUnicode:Will Stetson
Creator:Will Stetson
Version:Woey's Extra
Source:OneRoom
Tags:花坂結衣 CV:M・A・O Hanasaka Yui M.A.O kantoku カントク one room ワンルーム mix remix yahuri mohab500 FrenZ396 Chanci -Laura- Corinn Akitoshi Denil25 Dafiely A_r_m_i_n Armin wiz khalifa Jenny Woey Mr Moseby FoxyGrandpa Foxy Grandpa fieryrage
BeatmapID:1851299
BeatmapSetID:859783
[Difficulty]
HPDrainRate:6
CircleSize:3.8
OverallDifficulty:9.1
ApproachRate:9.2
SliderMultiplier:1.80000000596047
SliderTickRate:1
[Events]
//Background and Video events
0,0,"exbg.jpg",0,0
//Break Periods
//Storyboard Layer 0 (Background)
//Storyboard Layer 1 (Fail)
//Storyboard Layer 2 (Pass)
//Storyboard Layer 3 (Foreground)
//Storyboard Sound Samples
[TimingPoints]
154,352.941176470588,4,2,22,47,1,0
1919,-125,4,2,22,47,0,0
2154,-125,4,2,22,47,0,0
13919,-125,4,1,47,75,0,0
14272,-90.9090909090909,4,2,47,75,0,0
24242,-90.9090909090909,4,2,47,75,0,0
24331,-90.9090909090909,4,2,47,75,0,0
29625,-90.9090909090909,4,2,47,75,0,0
29801,-90.9090909090909,4,2,47,75,0,0
30154,-90.9090909090909,4,2,47,75,0,0
30507,-90.9090909090909,4,2,47,75,0,0
30860,-90.9090909090909,4,2,47,75,0,0
31213,-90.9090909090909,4,2,47,75,0,0
32624,-90.9090909090909,4,2,47,15,0,0
[Colours]
Combo1 : 255,43,48
Combo2 : 239,186,239
Combo3 : 205,218,165
Combo4 : 152,211,231
Combo5 : 239,226,186
[HitObjects]
256,192,2154,5,2,0:0:0:0:
312,180,2272,2,0,P|344:130|334:66,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
210,125,2625,2,0,P|178:175|188:239,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
299,303,2978,6,0,L|414:291,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
465,200,3331,2,0,L|369:209,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
323,384,3684,2,0,L|438:372,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
276,219,4037,2,0,L|180:228,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
111,293,4390,6,0,P|178:323|222:314,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
249,294,4742,1,2,0:0:0:0:
203,109,4978,2,0,P|82:154|74:216,1,192.000000635783,2|2,0:0|0:0,0:0:0:0:
329,209,5684,5,2,0:0:0:0:
315,153,5801,2,0,L|290:47,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
423,181,6154,2,0,L|400:274,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
207,234,6507,2,0,L|229:140,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
378,27,6860,2,0,P|426:69|429:118,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
400,274,7213,6,0,P|352:271|314:244,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
300,188,7566,1,2,0:0:0:0:
495,319,7801,2,0,P|414:362|291:339,1,192.000000635783,2|2,0:0|0:0,0:0:0:0:
169,265,8507,5,2,0:0:0:0:
218,235,8625,2,0,P|269:225|318:259,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
206,335,8978,2,0,P|155:345|106:311,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
87,276,9331,1,2,0:0:0:0:
289,325,9566,1,2,0:0:0:0:
289,325,9684,1,2,0:0:0:0:
175,164,9919,6,0,P|299:144|347:167,1,144.000000476838,2|2,0:0|0:0,0:0:0:0:
370,266,10390,2,0,P|315:224|306:172,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
312,148,10742,1,2,0:0:0:0:
475,241,10978,1,2,0:0:0:0:
475,241,11095,1,2,0:0:0:0:
403,57,11331,5,2,0:0:0:0:
403,57,11448,2,0,P|441:49|481:60,1,48.0000001589459,2|0,0:0|0:0,0:0:0:0:
512,165,11684,1,2,0:0:0:0:
290,249,12037,1,2,0:0:0:0:
318,12,12389,1,2,0:0:0:0:
58,105,12742,6,0,P|94:183|146:139,1,144.000000476838,2|2,0:0|0:0,0:0:0:0:
206,88,13213,2,0,P|265:77|305:98,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
334,221,13565,2,0,P|294:175|293:130,1,96.0000003178917,2|2,0:0|0:0,0:0:0:0:
398,78,13919,5,0,1:0:0:0:
398,78,14007,1,0,1:0:0:0:
398,78,14095,1,0,1:0:0:0:
203,217,14272,5,2,1:0:0:0:
449,215,14448,1,8,0:0:0:0:
274,168,14625,5,2,1:0:0:0:
489,292,14801,1,8,0:0:0:0:
234,329,14978,5,2,1:0:0:0:
449,215,15154,1,10,0:0:0:0:
203,217,15331,5,2,1:0:0:0:
481,373,15507,1,10,0:0:0:0:
136,273,15684,6,0,P|117:213|143:155,1,99.0000033490662,2|10,1:0|0:0,0:0:0:0:
280,55,16037,1,2,1:0:0:0:
360,266,16213,2,0,P|300:285|242:259,1,99.0000033490662,10|0,0:0|1:0,0:0:0:0:
178,85,16566,1,8,0:0:0:0:
353,131,16742,1,0,1:0:0:0:
233,207,16919,1,10,0:0:0:0:
280,55,17095,5,2,1:0:0:0:
311,199,17272,1,10,0:0:0:0:
366,38,17448,5,2,1:0:0:0:
381,245,17625,1,10,0:0:0:0:
264,21,17801,5,2,1:0:0:0:
244,266,17978,1,10,0:0:0:0:
153,6,18154,5,2,1:0:0:0:
96,249,18331,1,10,0:0:0:0:
331,101,18507,5,2,1:0:0:0:
88,78,18684,1,8,0:0:0:0:
375,19,18860,1,2,1:0:0:0:
132,160,19037,2,0,P|191:205|250:197,1,99.0000033490662,10|0,0:0|1:0,0:0:0:0:
420,170,19390,1,8,0:0:0:0:
202,291,19566,1,0,1:0:0:0:
331,101,19742,1,10,0:0:0:0:
291,360,19919,5,2,1:0:0:0:
318,189,20095,1,10,0:0:0:0:
175,357,20272,5,2,1:0:0:0:
148,186,20448,1,10,0:0:0:0:
353,282,20625,5,2,1:0:0:0:
315,39,20801,1,10,0:0:0:0:
223,282,20978,5,2,1:0:0:0:
185,39,21154,1,10,0:0:0:0:
401,202,21331,5,0,1:0:0:0:
256,172,21507,1,10,0:0:0:0:
471,141,21684,5,2,1:0:0:0:
441,286,21860,1,10,0:0:0:0:
326,111,22037,5,2,1:0:0:0:
402,381,22213,1,10,0:0:0:0:
402,54,22390,5,2,1:0:0:0:
326,324,22566,1,10,0:0:0:0:
268,31,22742,6,0,P|167:29|111:114,1,198.000006698132,6|0,1:2|1:0,0:0:0:0:
213,314,23272,2,0,B|232:268|220:220|216:220|212:220|192:180|214:120,1,198.000006698132,12|8,0:0|0:0,0:0:0:0:
321,26,23801,1,0,1:0:0:0:
481,201,23978,2,0,P|431:253|333:171,1,198.000006698132,14|2,0:0|1:0,0:0:0:0:
456,41,24507,1,10,0:0:0:0:
498,300,24684,2,0,P|414:331|303:289,1,198.000006698132,14|2,0:0|1:0,0:0:0:0:
220,82,25213,1,10,0:0:0:0:
383,148,25390,1,10,0:0:0:0:
251,245,25566,5,6,1:2:0:0:
276,13,25742,1,8,0:0:0:0:
454,192,25919,5,4,1:2:0:0:
188,162,26095,1,8,0:0:0:0:
395,3,26272,5,4,1:2:0:0:
364,283,26448,1,10,0:0:0:0:
169,75,26625,5,4,1:2:0:0:
487,111,26801,1,10,0:0:0:0:
277,326,26978,5,4,1:2:0:0:
259,42,27154,1,8,0:0:0:0:
371,361,27331,5,6,1:2:0:0:
364,60,27507,1,10,0:0:0:0:
254,344,27684,5,4,1:2:0:0:
284,26,27860,1,10,0:0:0:0:
126,363,28037,5,4,1:2:0:0:
195,22,28213,1,10,0:0:0:0:
179,289,28390,6,0,P|125:257|122:133,1,198.000006698132,6|0,1:2|0:0,0:0:0:0:
398,195,28919,2,0,B|356:180|304:188|304:192|304:196|260:216|204:193,1,198.000006698132,6|0,1:2|0:0,0:0:0:0:
47,39,29448,5,6,1:2:0:0:
256,192,29625,12,0,32977,0:0:0:0:
+391
View File
@@ -0,0 +1,391 @@
osu file format v8
[General]
AudioFilename: tutorial2.ogg
AudioLeadIn: 0
PreviewTime: 52620
Countdown: 0
SampleSet: Normal
StackLeniency: 0.7
Mode: 0
LetterboxInBreaks: 0
UseSkinSprites: 1
[Editor]
Bookmarks: 42132
DistanceSpacing: 1.39999997615814
BeatDivisor: 3
GridSize: 8
[Metadata]
Title:osu! tutorial
Artist:Peter Lambert
Creator:Sushi
Version:Advanced Gameplay
Source:
Tags:Agnes
[Difficulty]
HPDrainRate:4
CircleSize:4
OverallDifficulty:6
ApproachRate:7
SliderMultiplier:1.5
SliderTickRate:1
[Events]
//Background and Video events
0,0,"bg.jpg"
//Break Periods
//Storyboard Layer 0 (Background)
Sprite,Background,Centre,"black.png",320,240
M,0,-2119,-124,320,240
M,0,-124,6235,320,240
F,0,5861,6235,1,0
Sprite,Background,Centre,"osulogo.png",320,250
L,1746,11
S,1,0,374,1.1,1
F,0,24,607,0,1
M,1,24,1746,0,250,320,250
R,1,24,1746,-4,0
F,1,5861,6235,1,0
S,0,5861,6235,1,3
Sprite,Background,Centre,"black.png",320,240
F,0,122709,122833,0,1
F,0,122833,125951,1,0.984
//Storyboard Layer 1 (Fail)
//Storyboard Layer 2 (Pass)
Sprite,Pass,Centre,"endpass.png",320,240
M,0,119965,,316,155
F,0,119965,120090,0,1
S,1,119965,120090,0.1,1
F,0,120090,,1
F,0,120090,122958,1
F,0,122958,123083,1,0
F,0,123083,,0
Sprite,Pass,Centre,"inorite.png",320,240
M,0,121462,,315,356
S,0,121462,,0.4
F,0,121462,121836,0,1
F,0,121836,122958,1
F,0,122958,123332,1,0
//Storyboard Layer 3 (Foreground)
Sprite,Foreground,Centre,"howto3.png",320,180
L,1746,2
S,1,0,374,0.8,0.7
S,1,374,747,0.8,0.7
R,0,746,1120,-0.1,-6.38
S,1,748,1120,0.8,0.7
S,1,1122,1495,0.8,0.7
L,4739,2
S,1,0,374,0.8,0.7
M,0,1024,1624,310,515,314,319
F,0,1024,1624,0,1
S,0,1024,1624,0.7
R,0,1024,1624,0.5,-0.1
R,0,2494,2868,-0.1,-6.38
F,0,5487,5861,1,0
S,0,5487,5861,0.7,3
Sprite,Foreground,Centre,"text1.png",320,240
M,1,6235,6609,156,73,239,73
F,2,6235,6609,0,1
M,0,6609,,239,73
F,0,6609,22696,1
F,0,22696,22821,1,0
Sprite,Foreground,Centre,"text2.png",320,240
M,1,8480,8854,513,115,322,115
F,2,8480,8854,0,1
F,0,8854,22696,1,0.9973262
F,0,22696,22946,0.9973262,0
Sprite,Foreground,Centre,"text3.png",320,240
M,1,10725,11099,76,144,212,140
F,2,10725,11099,0,1
F,0,11099,22696,1,0.9946524
F,0,22696,23070,0.9946524,0
Sprite,Foreground,Centre,"text4.png",320,240
M,1,12969,13343,537,179,374,179
F,2,12969,13343,0,1
F,0,13343,22696,1,0.9946524
F,0,22696,23195,0.9946524,0
Sprite,Foreground,Centre,"text5.png",320,240
M,1,15214,15588,79,217,282,211
F,2,15214,15588,0,1
F,0,15588,22696,1
F,0,22696,23320,1,0
Sprite,Foreground,Centre,"text6.png",320,240
M,1,17459,17833,545,249,417,249
F,2,17459,17833,0,1
F,0,17833,22696,1
F,0,22696,23444,1,0
Sprite,Foreground,Centre,"text7.png",320,240
M,1,18955,19329,-20,286,62,286
F,2,18955,19329,0,1
M,0,19329,,62,286
F,0,19329,22696,1,0.9629171
F,0,22696,23569,0.9629171,0
Sprite,Foreground,Centre,"text8.png",320,240
M,1,20452,20826,560,358,321,358
F,2,20452,20826,0,1
F,0,20826,22696,1
F,0,22696,23694,1,0
Sprite,Foreground,Centre,"osulogo.png",320,240
S,0,122335,122709,3.032
F,0,122335,122833,0,1
S,0,122709,123582,3.032,0.968
F,0,122833,,1
F,0,122833,124455,1
F,0,124455,124829,1,0
//Storyboard Sound Samples
//Background Colour Transformations
3,100,128,128,255
[TimingPoints]
250,374.111485222596,4,1,0,58,1,0
29377,-100,4,2,0,69,0,0
29939,-100,4,1,0,58,0,0
35363,-100,4,2,0,69,0,0
35971,-100,4,2,0,100,0,0
40461,-100,4,1,0,58,0,0
41349,-100,4,2,0,69,0,0
41910,-100,4,2,0,100,0,0
46518,-100,4,1,0,58,0,0
47335,-100,4,2,0,69,0,0
47943,-100,4,2,0,100,0,0
52451,-100,4,1,0,58,0,0
53321,-100,4,2,0,69,0,0
53929,-100,4,2,0,100,0,0
58980,-100,4,1,0,74,0,0
96018,-100,4,1,0,74,0,1
119588,-100,4,1,0,74,0,0
[Colours]
Combo1 : 0,255,0
Combo2 : 255,0,0
Combo3 : 0,0,255
Combo4 : 255,255,0
[HitObjects]
256,328,24193,2,0,C|424:328,2,150
256,192,25315,1,8
256,56,25689,6,0,C|96:56,2,150
256,192,26811,1,8
136,128,27186,5,0
16,192,27560,1,0
136,256,27934,1,0
256,192,28308,1,8
256,328,28682,6,0,C|304:328,3,50
440,328,29430,1,4
440,328,29804,1,4
440,192,30178,6,0,C|480:152,3,50
376,64,30927,1,0
376,64,31301,1,8
312,192,31675,6,0,C|264:144,3,50
376,64,32423,1,0
376,64,32797,1,8
376,200,33171,5,0
376,336,33545,1,0
376,200,33920,1,0
376,336,34294,1,8
240,336,34668,6,0,C|176:336,3,50
256,192,35416,5,4
256,192,35790,1,4
144,112,36164,6,0,C|72:144|96:240,2,150,0|2|0
256,192,37287,1,2
368,272,37661,6,0,C|440:240|416:144,2,150,0|2|0
256,192,38783,1,2
192,72,39157,6,0,C|128:232,1,150,0|2
320,72,39905,2,0,C|384:232,1,150,0|2
256,280,40654,6,0,C|256:352,3,50
256,192,41402,1,0
256,192,41776,1,4
392,192,42150,5,0
392,192,42275,1,0
392,192,42399,1,0
392,192,42524,2,0,B|392:288|312:304,2,150,2|0|2
392,56,43646,5,0
392,56,43771,1,0
392,56,43896,1,0
392,56,44021,2,0,B|296:56|280:136,2,150,2|0|2
344,184,45143,5,0
344,184,45268,1,0
344,184,45392,1,0
344,184,45517,2,0,B|280:256|216:184,2,150,2|0|2
344,320,46639,6,0,B|192:320,1,150
194,320,47138,1,0
194,320,47263,1,0
194,320,47388,1,0
56,320,47762,1,4
56,184,48136,6,0,B|56:96|152:96,1,150,0|2
256,192,48884,1,2
256,192,49009,1,0
256,192,49133,1,0
256,192,49258,1,2
456,200,49632,6,0,B|456:288|360:288,1,150,0|2
256,192,50380,1,2
256,192,50505,1,0
256,192,50630,1,0
256,192,50755,1,2
184,72,51129,5,0
328,72,51503,1,2
184,72,51877,1,0
328,72,52251,1,2
256,192,52625,6,0,B|256:352,1,150
256,342,53124,1,0
256,342,53249,1,0
256,342,53373,1,0
392,344,53747,1,4
392,208,54122,6,0,B|456:208,3,50,0|0|0|2
392,80,54870,1,2
392,80,54994,1,0
392,80,55119,1,0
392,80,55244,1,2
256,80,55618,6,0,B|256:176|152:176,1,150,0|2
64,80,56366,1,2
64,128,56491,1,0
64,176,56616,1,0
64,224,56740,1,2
184,280,57114,6,0,B|184:344,3,50,0|0|0|2
328,280,57863,2,0,B|328:344,3,50,2|0|0|2
256,192,58611,12,4,59733
256,48,60107,6,0,B|352:48|352:136,2,150,4|8|0
200,80,61043,1,8
160,136,61230,1,2
144,200,61417,1,10
144,264,61604,6,0,B|152:360|264:320,2,150,0|8|0
144,200,62539,1,0
208,176,62726,1,10
272,192,62913,1,0
328,224,63100,6,0,B|432:256|416:152,2,150,4|8|0
272,192,64036,1,8
256,128,64223,1,2
296,72,64410,1,10
256,192,64597,12,8,65719
256,72,66093,5,4
192,72,66280,1,0
128,72,66467,2,0,B|56:104|72:208,2,150,8|0|10
192,72,67403,1,2
240,120,67590,5,0
192,168,67777,2,0,B|160:248,2,75,8|0|0
248,200,68338,2,0,B|264:280,3,75,0|0|10|0
320,240,69086,6,0,B|472:240,1,150,4|8
470,173,69647,1,0
470,105,69834,2,8,B|320:105,1,150,0|10
256,192,70582,12,8,71705
256,64,72079,6,0,B|416:64,1,150,4|8
288,128,72827,1,0
352,128,73014,1,0
416,128,73201,1,10
320,224,73575,6,0,B|152:224,1,150,0|8
288,288,74324,1,0
224,288,74511,1,0
160,288,74698,1,10
64,192,75072,6,0,B|64:88|152:88,1,150,0|8
216,88,75633,1,0
280,88,75820,1,0
280,152,76007,1,0
216,152,76194,1,10
256,192,76568,12,8,77691
256,48,78065,6,0,B|344:72,1,75
400,32,78439,2,0,B|424:120,1,75,8|0
464,168,78813,2,0,B|408:256,1,75
352,224,79187,1,10
256,48,79561,6,0,B|168:72,1,75
112,32,79935,2,0,B|88:120,1,75,8|0
48,168,80309,2,0,B|104:256,1,75
160,224,80683,1,10
208,368,81058,6,0,B|176:280,1,75
256,197,81432,2,0,B|256:272,1,75,8|0
304,368,81806,2,0,B|336:280,1,75,0|0
352,224,82180,1,10
256,192,82554,12,4,83676
256,48,84050,5,8
256,48,84175,1,0
256,48,84300,1,0
256,48,84425,2,2,B|160:48|160:136,1,150,2|8
40,48,85173,1,2
32,200,85547,5,8
32,200,85672,1,0
32,200,85796,1,0
32,200,85921,2,2,B|32:296|120:296,2,150,2|8|8
256,336,87043,5,8
256,336,87168,1,0
256,336,87293,1,0
256,336,87417,2,2,B|352:336|352:248,1,150,2|8
472,336,88166,1,2
480,184,88540,5,8
480,184,88665,1,0
480,184,88789,1,0
480,184,88914,2,2,B|480:88|392:88,2,150,2|8|2
256,64,90036,6,0,B|256:184,1,100,8|0
256,216,90410,1,2
184,352,90784,1,8
232,352,90909,1,0
280,352,91034,1,0
328,352,91159,1,2
320,200,91533,6,0,B|384:200|384:136,1,100,8|0
384,88,91907,1,2
472,208,92281,1,8
448,256,92406,1,0
424,304,92530,1,0
400,352,92655,2,2,B|296:352,1,100,2|2
256,192,93029,12,0,95648
376,96,96022,6,0,B|472:96|472:192,2,150,8|8|0
328,96,96895,1,0
280,96,97020,1,0
232,96,97144,1,8
168,232,97518,6,0,B|72:232|72:136,2,150,8|8|0
216,232,98391,1,0
264,232,98516,1,0
312,232,98641,1,8
256,96,99015,6,0,B|416:96,2,150,8|8|0
256,96,99888,1,0
256,96,100013,1,0
256,96,100137,1,8
256,248,100511,6,0,B|256:304,2,50
256,200,100885,2,0,B|256:136,3,50,8|0|0|4
106,150,101634,1,4
96,288,102008,6,0,B|80:344,2,50,8|0|0
104,240,102382,1,2
216,320,102756,1,0
216,184,103130,1,10
336,128,103504,6,0,B|352:72,2,50,8|0|0
328,176,103878,1,2
216,96,104252,1,0
216,232,104627,1,10
328,304,105001,5,8
328,304,105125,1,0
328,304,105250,1,0
328,304,105375,2,0,B|416:304|416:208,1,150,2|0
416,72,106123,1,10
256,192,106497,12,4,107619
256,56,107994,6,0,B|144:56,1,100
96,56,108368,2,0,B|40:152,1,100,10|2
40,200,108742,2,0,B|40:272,3,50,0|0|0|10
200,336,109490,6,0,B|296:256,1,100
312,320,109864,2,2,B|408:240,1,100,10|2
424,304,110238,2,0,B|464:272,3,50,0|0|0|10
424,96,110986,6,0,B|376:64|328:96,1,100
328,152,111361,2,2,B|280:184|232:152,1,100,10|2
224,96,111735,2,0,B|200:80|176:96,3,50,0|0|0|10
256,256,112483,5,0
256,256,112608,1,0
256,256,112732,1,0
256,256,112857,1,8
256,256,112982,1,0
256,256,113106,1,0
256,256,113231,2,0,B|208:312|320:360|328:296,1,150,4|4
120,208,113979,6,0,B|56:208,3,50,8|0|0|2
120,80,114728,1,8
120,80,114852,1,0
120,80,114977,1,0
120,80,115102,1,2
256,80,115476,6,0,B|256:176|360:176,1,150,8|2
448,80,116224,1,8
448,128,116349,1,0
448,176,116473,1,0
448,224,116598,1,2
256,192,116972,12,8,118095
184,304,118469,6,0,B|296:304,1,100,0|8
320,248,118843,2,0,B|208:248,1,100,0|8
184,192,119217,1,4
184,192,119591,1,4