diff --git a/Cargo.lock b/Cargo.lock index 28a981d..d76acaf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1038,7 +1038,7 @@ checksum = "c7d73b3f436185384286bd8098d17ec07c9a7d2388a6599f824d8502b529702a" [[package]] name = "libosu" version = "0.0.12" -source = "git+https://github.com/iptq/libosu?rev=d75fe384e95156b3b40dd9e5ca7af539e48af8fa#d75fe384e95156b3b40dd9e5ca7af539e48af8fa" +source = "git+https://github.com/iptq/libosu?rev=f15cfd0c6331ba7ad76cc0acc0eb5c11cb145cb0#f15cfd0c6331ba7ad76cc0acc0eb5c11cb145cb0" dependencies = [ "anyhow", "bitflags", @@ -1047,6 +1047,7 @@ dependencies = [ "num-derive 0.3.3", "num-rational 0.3.2", "num-traits 0.2.14", + "ordered-float 2.0.1", "regex", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index 5f02129..ac06798 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,4 +20,4 @@ ordered-float = "2.0.1" [dependencies.libosu] git = "https://github.com/iptq/libosu" -rev = "d75fe384e95156b3b40dd9e5ca7af539e48af8fa" +rev = "f15cfd0c6331ba7ad76cc0acc0eb5c11cb145cb0" diff --git a/rust-toolchain b/rust-toolchain deleted file mode 100644 index bf867e0..0000000 --- a/rust-toolchain +++ /dev/null @@ -1 +0,0 @@ -nightly diff --git a/src/beatmap.rs b/src/beatmap.rs new file mode 100644 index 0000000..f8f99e3 --- /dev/null +++ b/src/beatmap.rs @@ -0,0 +1,68 @@ +use libosu::{Beatmap, HitObjectKind, Point}; + +use crate::hit_object::HitObjectExt; + +const STACK_DISTANCE: f64 = 3.0; + +pub struct BeatmapExt { + pub inner: Beatmap, + pub hit_objects: Vec, +} + +impl BeatmapExt { + pub fn new(inner: Beatmap) -> Self { + let hit_objects = inner + .hit_objects + .iter() + .cloned() + .map(HitObjectExt::new) + .collect(); + + BeatmapExt { inner, hit_objects } + } + + pub fn compute_stacking(&mut self, start_idx: usize, end_idx: usize) { + let mut extended_end_idx = end_idx; + + if end_idx < self.hit_objects.len() - 1 { + // Extend the end index to include objects they are stacked on + for i in (start_idx..=end_idx).rev() { + let mut stack_base_idx = i; + + for n in stack_base_idx + 1..self.hit_objects.len() { + let stack_base_obj = &self.hit_objects[stack_base_idx]; + if let HitObjectKind::Spinner(_) = &stack_base_obj.inner.kind { + break; + } + + let object_n = &self.hit_objects[n]; + if let HitObjectKind::Spinner(_) = &object_n.inner.kind { + break; + } + + let end_time = self.inner.get_hitobject_end_time(&stack_base_obj.inner); + let stack_threshold = + self.inner.difficulty.approach_preempt() as f64 * self.inner.stack_leniency; + + // We are no longer within stacking range of the next object. + if (object_n.inner.start_time.0 - end_time.0) as f64 > stack_threshold { + break; + } + + let stack_base_pos: Point = stack_base_obj.inner.pos.to_float().unwrap(); + let object_n_pos: Point = object_n.inner.pos.to_float().unwrap(); + // if stack_base_pos.distance(object_n_pos) < STACK_DISTANCE + // || (stack_base_obj.inner.kind.is_slider() + // && self + // .inner + // .get_hitobject_end_pos(stack_base_obj) + // .distance(object_n_pos) + // < STACK_DISTANCE) + // {} + } + } + } + + let mut extended_start_idx = start_idx; + } +} diff --git a/src/game.rs b/src/game.rs index 7171987..088c5af 100644 --- a/src/game.rs +++ b/src/game.rs @@ -6,18 +6,19 @@ use std::path::Path; use anyhow::Result; use ggez::{ event::{EventHandler, KeyCode, KeyMods}, - nalgebra::Point2, graphics::{ self, Color, DrawMode, DrawParam, FillOptions, FilterMode, Mesh, Rect, StrokeOptions, Text, WHITE, }, + nalgebra::Point2, Context, GameError, GameResult, }; -use libosu::{Beatmap, HitObject, HitObjectKind, Point, SpinnerInfo}; +use libosu::{Beatmap, HitObject, HitObjectKind, Point, SpinnerInfo, Spline}; use crate::audio::{AudioEngine, Sound}; +use crate::beatmap::BeatmapExt; use crate::skin::Skin; -use crate::slider_render::{render_slider, Spline}; +use crate::slider_render::render_slider; pub type SliderCache = HashMap>, Spline>; @@ -25,8 +26,7 @@ pub struct Game { is_playing: bool, audio_engine: AudioEngine, song: Option, - beatmap: Beatmap, - hit_objects: Vec, + beatmap: BeatmapExt, pub skin: Skin, frame: usize, slider_cache: SliderCache, @@ -35,15 +35,15 @@ pub struct Game { impl Game { pub fn new() -> Result { let audio_engine = AudioEngine::new()?; - let beatmap = Beatmap::default(); - let hit_objects = Vec::new(); let skin = Skin::new(); + let beatmap = Beatmap::default(); + let beatmap = BeatmapExt::new(beatmap); + Ok(Game { is_playing: false, audio_engine, beatmap, - hit_objects, song: None, skin, frame: 0, @@ -57,11 +57,13 @@ impl Game { let mut file = File::open(&path)?; let mut contents = String::new(); file.read_to_string(&mut contents)?; - self.beatmap = Beatmap::from_osz(&contents)?; + + let beatmap = Beatmap::from_osz(&contents)?; + self.beatmap = BeatmapExt::new(beatmap); let dir = path.parent().unwrap(); - let song = Sound::create(dir.join(&self.beatmap.audio_filename))?; + let song = Sound::create(dir.join(&self.beatmap.inner.audio_filename))?; song.set_position(113.0)?; self.song = Some(song); @@ -105,7 +107,7 @@ impl Game { end_time: f64, } - let timeline_span = 6.0 / self.beatmap.timeline_zoom; + let timeline_span = 6.0 / self.beatmap.inner.timeline_zoom; let timeline_left = time - timeline_span / 2.0; let timeline_right = time + timeline_span / 2.0; println!("left {:.3} right {:.3}", timeline_left, timeline_right); @@ -125,13 +127,13 @@ impl Game { graphics::draw(ctx, ¤t_line, DrawParam::default())?; let mut playfield_hitobjects = Vec::new(); - let preempt = (self.beatmap.difficulty.approach_preempt() as f64) / 1000.0; - let fade_in = (self.beatmap.difficulty.approach_fade_time() as f64) / 1000.0; + let preempt = (self.beatmap.inner.difficulty.approach_preempt() as f64) / 1000.0; + let fade_in = (self.beatmap.inner.difficulty.approach_fade_time() as f64) / 1000.0; // TODO: tighten this loop even more by binary searching for the start of the timeline and // playfield hitobjects rather than looping through the entire beatmap, better yet, just // keeping track of the old index will probably be much faster - for ho in self.beatmap.hit_objects.iter().rev() { + for ho in self.beatmap.inner.hit_objects.iter().rev() { let ho_time = (ho.start_time.0 as f64) / 1000.0; // draw in timeline @@ -141,7 +143,7 @@ impl Game { let timeline_y = TIMELINE_BOUNDS.y; println!( " - [{}] {:.3}-{:.3} : {:.3}%", - self.beatmap.timeline_zoom, + self.beatmap.inner.timeline_zoom, timeline_left, timeline_right, timeline_percent * 100.0 @@ -173,7 +175,7 @@ impl Game { match ho.kind { HitObjectKind::Circle => end_time = ho_time, HitObjectKind::Slider(_) => { - let duration = self.beatmap.get_slider_duration(ho).unwrap(); + let duration = self.beatmap.inner.get_slider_duration(ho).unwrap(); end_time = ho_time + duration / 1000.0; } HitObjectKind::Spinner(SpinnerInfo { @@ -192,7 +194,7 @@ impl Game { let cs_scale = PLAYFIELD_BOUNDS.w / 640.0; let osupx_scale_x = PLAYFIELD_BOUNDS.w / 512.0; let osupx_scale_y = PLAYFIELD_BOUNDS.h / 384.0; - let cs_osupx = self.beatmap.difficulty.circle_size_osupx(); + let cs_osupx = self.beatmap.inner.difficulty.circle_size_osupx(); let cs_real = cs_osupx * cs_scale; for draw_info in playfield_hitobjects.iter() { @@ -210,7 +212,7 @@ impl Game { &mut self.slider_cache, ctx, PLAYFIELD_BOUNDS, - &self.beatmap, + &self.beatmap.inner, ho, color, )?; diff --git a/src/hit_object.rs b/src/hit_object.rs new file mode 100644 index 0000000..e57d152 --- /dev/null +++ b/src/hit_object.rs @@ -0,0 +1,12 @@ +use libosu::HitObject; + +pub struct HitObjectExt { + pub inner: HitObject, + pub stacking: usize, +} + +impl HitObjectExt { + pub fn new(inner: HitObject) -> Self { + HitObjectExt { inner, stacking: 0 } + } +} diff --git a/src/main.rs b/src/main.rs index 13d9874..ab97e92 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,5 +1,3 @@ -#![feature(vec_into_raw_parts)] - #[macro_use] extern crate anyhow; #[macro_use] @@ -7,8 +5,9 @@ extern crate log; extern crate bass_sys as bass; mod audio; +mod beatmap; mod game; -mod math; +mod hit_object; mod skin; mod slider_render; diff --git a/src/math.rs b/src/math.rs deleted file mode 100644 index cc51bc0..0000000 --- a/src/math.rs +++ /dev/null @@ -1,29 +0,0 @@ -use std::marker::PhantomData; - -use libosu::Point; -use num::Float; - -#[derive(Default)] -pub struct Math(PhantomData); - -impl Math { - pub fn circumcircle(p1: Point, p2: Point, p3: Point) -> (Point, T) { - let (x1, y1) = (p1.0, p1.1); - let (x2, y2) = (p2.0, p2.1); - let (x3, y3) = (p3.0, p3.1); - - let two = num::cast::<_, T>(2.0).unwrap(); - let d = two.mul_add(x1 * (y2 - y3) + x2 * (y3 - y1) + x3 * (y1 - y2), T::zero()); - let ux = ((x1 * x1 + y1 * y1) * (y2 - y3) - + (x2 * x2 + y2 * y2) * (y3 - y1) - + (x3 * x3 + y3 * y3) * (y1 - y2)) - / d; - let uy = ((x1 * x1 + y1 * y1) * (x3 - x2) - + (x2 * x2 + y2 * y2) * (x1 - x3) - + (x3 * x3 + y3 * y3) * (x2 - x1)) - / d; - - let center = Point(ux, uy); - (center, center.distance(p1)) - } -} diff --git a/src/slider_render.rs b/src/slider_render.rs index 12af257..77a75e3 100644 --- a/src/slider_render.rs +++ b/src/slider_render.rs @@ -1,5 +1,3 @@ -use std::collections::VecDeque; - use anyhow::Result; use ggez::{ graphics::{ @@ -8,11 +6,9 @@ use ggez::{ nalgebra::Point2, Context, }; -use libosu::{Beatmap, HitObject, HitObjectKind, Point, SliderSplineKind}; -use ordered_float::NotNan; +use libosu::{Beatmap, HitObject, HitObjectKind, Spline}; use crate::game::SliderCache; -use crate::math::Math; pub fn render_slider<'a>( slider_cache: &'a mut SliderCache, @@ -33,7 +29,7 @@ pub fn render_slider<'a>( slider_cache.get(&control_points).unwrap() } else { let new_spline = - Spline::from_control(&slider_info.kind, &control_points, slider_info.pixel_length); + Spline::from_control(slider_info.kind, &control_points, slider_info.pixel_length); slider_cache.insert(control_points.clone(), new_spline); slider_cache.get(&control_points).unwrap() }; @@ -99,277 +95,3 @@ pub fn render_slider<'a>( Ok(spline) } - -pub struct Spline { - spline_points: Vec

, - cumulative_lengths: Vec>, -} - -impl Spline { - fn from_control( - kind: &SliderSplineKind, - control_points: &[Point], - pixel_length: f64, - ) -> Self { - // no matter what, if there's 2 control points, it's linear - let mut kind = kind.clone(); - if control_points.len() == 2 { - kind = SliderSplineKind::Linear; - } - - let points = control_points - .iter() - .map(|p| Point(p.0 as f64, p.1 as f64)) - .collect::>(); - let spline_points = match kind { - SliderSplineKind::Linear => { - let start = points[0]; - let unit = (points[1] - points[0]).norm(); - let end = points[0] + unit * pixel_length; - vec![start, end] - } - SliderSplineKind::Perfect => { - let (p1, p2, p3) = (points[0], points[1], points[2]); - let (center, radius) = Math::circumcircle(p1, p2, p3); - - // find the t-values of the start and end of the slider - let t0 = (center.1 - p1.1).atan2(p1.0 - center.0); - let mut mid = (center.1 - p2.1).atan2(p2.0 - center.0); - let mut t1 = (center.1 - p3.1).atan2(p3.0 - center.0); - - // make sure t0 is less than t1 - while mid < t0 { - mid += std::f64::consts::TAU; - } - while t1 < t0 { - t1 += std::f64::consts::TAU; - } - if mid > t1 { - t1 -= std::f64::consts::TAU; - } - - // circumference is 2 * pi * r, slider length over circumference is length/(2 * pi * r) - let direction_unit = (t1 - t0) / (t1 - t0).abs(); - let new_t1 = t0 + direction_unit * (pixel_length / radius); - - let mut t = t0; - let mut c = Vec::new(); - loop { - if !((new_t1 >= t0 && t < new_t1) || (new_t1 < t0 && t > new_t1)) { - break; - } - - let rel = Point(t.cos() * radius, -t.sin() * radius); - c.push(center + rel); - - t += (new_t1 - t0) / pixel_length; - } - c - } - SliderSplineKind::Bezier => { - let mut idx = 0; - let mut whole = Vec::new(); - let mut cumul_length = 0.0; - let mut last_circ: Option

= None; - let mut check_push = |whole: &mut Vec

, point: P| -> bool { - if cumul_length < pixel_length { - whole.push(point); - if let Some(circ) = last_circ { - cumul_length += circ.distance(point); - } - last_circ = Some(point); - true - } else { - false - } - }; - - // split the curve by red-anchors - for i in 1..points.len() { - if points[i].0 == points[i - 1].0 && points[i].1 == points[i - 1].1 { - let spline = calculate_bezier(&points[idx..i]); - - // check if it's equal to the last thing that was added to whole - if let Some(last) = whole.last() { - if spline[0] != *last { - check_push(&mut whole, spline[0]); - } - } else { - check_push(&mut whole, spline[0]); - } - - // add points, making sure no 2 are the same - for points in spline.windows(2) { - if points[0] != points[1] { - if !check_push(&mut whole, points[1]) { - break; - } - } - } - - idx = i; - continue; - } - } - - let spline = calculate_bezier(&points[idx..]); - if let Some(last) = whole.last() { - if spline[0] != *last { - check_push(&mut whole, spline[0]); - } - } else { - check_push(&mut whole, spline[0]); - } - for points in spline.windows(2) { - if points[0] != points[1] { - if !check_push(&mut whole, points[1]) { - break; - } - } - } - whole - } - _ => todo!(), - }; - - let mut cumulative_lengths = Vec::with_capacity(spline_points.len()); - let mut curr = 0.0; - // using NotNan here because these need to be binary-searched over - // and f64 isn't Ord - cumulative_lengths.push(unsafe { NotNan::unchecked_new(curr) }); - for points in spline_points.windows(2) { - let dist = points[0].distance(points[1]); - curr += dist; - cumulative_lengths.push(unsafe { NotNan::unchecked_new(curr) }); - } - - Spline { - spline_points, - cumulative_lengths, - } - } - - pub fn point_at_length(&self, length: f64) -> P { - let length_notnan = unsafe { NotNan::unchecked_new(length) }; - match self.cumulative_lengths.binary_search(&length_notnan) { - Ok(idx) => self.spline_points[idx], - Err(idx) => { - let n = self.spline_points.len(); - if idx == 0 && self.spline_points.len() > 2 { - return self.spline_points[0]; - } else if idx == n { - return self.spline_points[n - 1]; - } - - let (len1, len2) = ( - self.cumulative_lengths[idx - 1].into_inner(), - self.cumulative_lengths[idx].into_inner(), - ); - let proportion = (length - len1) / (len2 - len1); - - let (p1, p2) = (self.spline_points[idx - 1], self.spline_points[idx]); - // println!( - // "len={:.3} idx={} len1={:.3} len2={:.3} prop={:.3} p1={:.3} p2={:.3}", - // length, idx, len1, len2, proportion, p1, p2 - // ); - assert!(p1 != p2); - (p2 - p1) * proportion + p1 - } - } - } -} - -type P = Point; -type V = (*mut T, usize, usize); -fn calculate_bezier(points: &[P]) -> Vec

{ - let points = points.to_vec(); - let mut output = Vec::new(); - let n = points.len() - 1; - let last = points[n]; - - let mut to_flatten = VecDeque::new(); - let mut free_buffers = VecDeque::new(); - - to_flatten.push_back(points.into_raw_parts()); - let mut p = n; - let buf1 = vec![Point(0.0, 0.0); p + 1].into_raw_parts(); - let buf2 = vec![Point(0.0, 0.0); p * 2 + 1].into_raw_parts(); - - let left_child = buf2; - while !to_flatten.is_empty() { - let parent = to_flatten.pop_front().unwrap(); - let parent_slice = unsafe { std::slice::from_raw_parts_mut(parent.0, parent.1) }; - - if bezier_flat_enough(parent_slice) { - bezier_approximate(parent_slice, &mut output, buf1, buf2, p + 1); - free_buffers.push_front(parent); - continue; - } - - let right_child = if free_buffers.is_empty() { - let buf = vec![Point(0.0, 0.0); p + 1]; - buf.into_raw_parts() - } else { - free_buffers.pop_front().unwrap() - }; - bezier_subdivide(parent_slice, left_child, right_child, buf1, p + 1); - - let left_child = unsafe { std::slice::from_raw_parts(left_child.0, left_child.1) }; - for i in 0..p + 1 { - parent_slice[i] = left_child[i]; - } - - to_flatten.push_front(right_child); - to_flatten.push_front(parent); - } - - output.push(last); - output -} - -const TOLERANCE: f64 = 0.25; -fn bezier_flat_enough(curve: &[P]) -> bool { - for i in 1..(curve.len() - 1) { - let p = curve[i - 1] - curve[i] * 2.0 + curve[i + 1]; - if p.0 * p.0 + p.1 * p.1 > TOLERANCE * TOLERANCE / 4.0 { - return false; - } - } - true -} - -fn bezier_approximate(curve: &[P], output: &mut Vec

, buf1: V

, buf2: V

, count: usize) { - let l = buf2; - let r = buf1; - bezier_subdivide(curve, l, r, buf1, count); - - let l = unsafe { std::slice::from_raw_parts_mut(l.0, l.1) }; - let r = unsafe { std::slice::from_raw_parts_mut(r.0, r.1) }; - for i in 0..(count - 1) { - l[count + i] = r[i + 1]; - } - output.push(curve[0]); - - for i in 1..(count - 1) { - let idx = 2 * i; - let p = (l[idx - 1] + l[idx] * 2.0 + l[idx + 1]) * 0.25; - output.push(p); - } -} - -fn bezier_subdivide(curve: &[P], l: V

, r: V

, subdiv: V

, count: usize) { - let midpoints = unsafe { std::slice::from_raw_parts_mut(subdiv.0, subdiv.1) }; - for i in 0..count { - midpoints[i] = curve[i]; - } - - let l = unsafe { std::slice::from_raw_parts_mut(l.0, l.1) }; - let r = unsafe { std::slice::from_raw_parts_mut(r.0, r.1) }; - for i in 0..count { - l[i] = midpoints[0]; - r[count - i - 1] = midpoints[count - i - 1]; - for j in 0..(count - i - 1) { - midpoints[j] = (midpoints[j] + midpoints[j + 1]) * 0.5; - } - } -}