vsrg 0.3.0

Data structures for vertical scrolling rhythm games
Documentation
use std::collections::HashMap;

use soa_rs::{Soa, SoaClone, Soars};

mod edit;
pub mod query;
#[cfg(test)]
mod test;

use crate::collections::IntervalTree;
use crate::notes::{GroupId, Interval};
use crate::rhythm::{ClockTime, ScrollPosition, Time, TrackId};
#[expect(unused)]
pub use edit::*;

/// Provides data that identifies a long note, and specifies its SoA types.
///
/// At runtime, each note is stored as a set of structure-of-arrays components:
/// - [`LongNoteData::ValueData`]: the serializable note value.
/// - [`LongNoteData::RuntimeData`]: runtime state supporting judgement and rendering,
///   initialized to its default when the note is added.
pub trait LongNoteData {
    type ValueData: Soars + SoaClone + Clone + 'static;
    type RuntimeData: Soars + SoaClone + Clone + Default + 'static;

    /// Start time of the note.
    fn start_time(&self) -> Time;
    /// Duration of the note.
    fn duration(&self) -> Time;
    /// The group id this note belongs to.
    fn group_id(&self) -> &GroupId;
    /// Data component holding the serializable note value.
    fn value_data(&self) -> Self::ValueData;

    /// Reconstruct the original note value from the structure-of-arrays components
    fn reconstruct<'a>(
        start_time: Time,
        duration: Time,
        value_data: <Self::ValueData as Soars>::Ref<'a>,
        group_id: GroupId,
    ) -> Self;

    /// Apply the event track to runtime state (optional).
    ///
    /// # Remarks.
    /// vsrg only caches the scroll position and clock time of the note's endpoints. If your game
    /// requires caching for midpoints as well, then this cache should live in runtime data. This
    /// method provides the relevant event tracks to compute that data.
    fn apply_event_track(
        _state: &mut Self::RuntimeData,
        _tempo_track: &crate::rhythm::TempoTrack,
        _scroll_track: &crate::rhythm::ScrollSpeedTrack,
    ) {
    }
}

#[derive(Debug, Clone, Copy)]
struct NoteLocation {
    group_index: usize,
    index_in_time_sorted_group: usize,
    index_in_scroll_sorted_group: usize,
}

/// Storage for all long notes of the same note type.
///
/// # Remarks
/// Notes of each group are stored separately in their own axis-specific group instance,
/// to allow for quickly filtering by groups or applying properties individually for each
/// group.
///
/// Additionally, it duplicates the notes into two tracks, one is indexed by time (useful
/// for filtering notes for judgement), and one by scroll position (useful for filtering notes
/// for rendering).
#[derive(Clone)]
pub struct LongNoteTypeStorage<Note: LongNoteData> {
    note_id_counter: u64,
    note_id_lookup: HashMap<u64, NoteLocation>,
    time_sorted_groups: Vec<TimeIndexedLongNoteGroup<Note>>,
    scroll_sorted_groups: Vec<ScrollIndexedLongNoteGroup<Note>>,
}

impl<N: std::fmt::Debug + LongNoteData> std::fmt::Debug for LongNoteTypeStorage<N> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("LongNoteTypeStorage")
            .field("note_id_counter", &self.note_id_counter)
            .field("note_id_lookup", &self.note_id_lookup)
            .field("time_sorted_groups", &self.time_sorted_groups)
            .field("scroll_sorted_groups", &self.scroll_sorted_groups)
            .finish()
    }
}

// Both concrete group types share components, but expose only their own query axis.
macro_rules! define_long_note_group {
    ($name:ident, $axis:ty, $index:ident, $intervals:ident, $query:ident, $doc:literal) => {
        #[doc = $doc]
        /// All component arrays follow the interval tree's vEB layout. Tree query
        /// returns indices into the component array.
        #[derive(Clone)]
        pub struct $name<Note: LongNoteData> {
            group_id: GroupId,
            $index: IntervalTree<$axis>,
            note_id: Vec<u64>,
            start_time: Vec<Time>,
            duration: Vec<Time>,
            clock_time: Vec<Interval<ClockTime>>,
            scroll_position: Vec<Interval<ScrollPosition>>,
            value_data: Soa<Note::ValueData>,
            runtime_data: Soa<Note::RuntimeData>,
        }

        impl<Note: LongNoteData> std::fmt::Debug for $name<Note> {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                f.debug_struct(stringify!($name))
                    .field("group_id", &self.group_id)
                    .field("note_id", &self.note_id)
                    .field("start_time", &self.start_time)
                    .field("duration", &self.duration)
                    .field("clock_time", &self.clock_time)
                    .field("scroll_position", &self.scroll_position)
                    .finish()
            }
        }

        impl<Note: LongNoteData> $name<Note> {
            pub fn new(group_id: GroupId) -> Self {
                Self {
                    group_id,
                    $index: IntervalTree::new(),
                    note_id: vec![],
                    start_time: vec![],
                    duration: vec![],
                    clock_time: vec![],
                    scroll_position: vec![],
                    value_data: Default::default(),
                    runtime_data: Default::default(),
                }
            }

            pub fn group_id(&self) -> &GroupId {
                &self.group_id
            }

            /// Interval tree whose query results directly index this group's components.
            pub fn $index(&self) -> &IntervalTree<$axis> {
                &self.$index
            }

            /// Query inclusive intervals, returning component indices in tree traversal order.
            pub fn $query(&self, range: Interval<$axis>) -> impl Iterator<Item = usize> + '_ {
                self.$index.query_intersecting(range)
            }

            /// Note ids aligned with the other component arrays.
            pub fn note_id(&self) -> &[u64] {
                &self.note_id
            }

            /// Start times of notes in this storage.
            #[inline]
            pub fn start_time(&self) -> &[Time] {
                &self.start_time
            }
            /// Durations of notes in this storage.
            #[inline]
            pub fn duration(&self) -> &[Time] {
                &self.duration
            }
            /// Clock times of notes in this storage.
            #[inline]
            pub fn clock_time(&self) -> &[Interval<ClockTime>] {
                &self.clock_time
            }
            /// Struct-of-array of the serializable note value components of notes in this storage.
            #[inline]
            pub fn value_data(&self) -> &Soa<Note::ValueData> {
                &self.value_data
            }
            /// Struct-of-array of the runtime state components of notes in this storage.
            #[inline]
            pub fn runtime_data(&self) -> &Soa<Note::RuntimeData> {
                &self.runtime_data
            }
            /// Scroll positions of each note in this group.
            #[inline]
            pub fn scroll_position(&self) -> &[Interval<ScrollPosition>] {
                &self.scroll_position
            }

            fn remove(&mut self, i: usize) -> Note::RuntimeData {
                self.note_id.remove(i);
                self.start_time.remove(i);
                self.duration.remove(i);
                self.clock_time.remove(i);
                self.scroll_position.remove(i);
                self.value_data.remove(i);
                self.runtime_data.remove(i)
            }

            fn rebuild(&mut self) {
                self.$index.intervals.clone_from(&self.$intervals);
                self.$index.rebuild(|i, j| {
                    self.note_id.swap(i, j);
                    self.start_time.swap(i, j);
                    self.duration.swap(i, j);
                    self.clock_time.swap(i, j);
                    self.scroll_position.swap(i, j);
                    self.value_data.swap(i, j);
                    self.runtime_data.swap(i, j);
                });
            }

            fn health_check(&self) {
                self.$index.health_check();
                debug_assert_eq!(self.$index.intervals, self.$intervals);
                let len = self.note_id.len();
                debug_assert!(len > 0);
                debug_assert_eq!(self.start_time.len(), len);
                debug_assert_eq!(self.duration.len(), len);
                debug_assert_eq!(self.clock_time.len(), len);
                debug_assert_eq!(self.scroll_position.len(), len);
                debug_assert_eq!(self.value_data.len(), len);
                debug_assert_eq!(self.runtime_data.len(), len);
            }
        }
    };
}

define_long_note_group!(
    TimeIndexedLongNoteGroup,
    ClockTime,
    time_index,
    clock_time,
    query_time_intersecting,
    "Long-note components indexed only by clock time."
);
define_long_note_group!(
    ScrollIndexedLongNoteGroup,
    ScrollPosition,
    scroll_index,
    scroll_position,
    query_scroll_intersecting,
    "Long-note components indexed only by scroll position."
);

impl<Note: LongNoteData> LongNoteTypeStorage<Note> {
    /// Note ids of notes in this storage, in unspecified order.
    #[inline]
    pub fn note_id(&self) -> impl Iterator<Item = u64> {
        self.note_id_lookup.keys().copied()
    }
    /// Despite the historical method name, components are not in linear sorted order.
    /// Nonempty note groups with components in the clock-time tree's vEB order.
    #[inline]
    pub fn time_sorted_groups(&self) -> &[TimeIndexedLongNoteGroup<Note>] {
        &self.time_sorted_groups
    }
    /// Despite the historical method name, components are not in linear sorted order.
    /// The same nonempty groups in the same group order, with components in the scroll-position tree's vEB order.
    #[inline]
    pub fn scroll_sorted_groups(&self) -> &[ScrollIndexedLongNoteGroup<Note>] {
        &self.scroll_sorted_groups
    }
}