vsrg 0.6.0

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

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

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

#[expect(unused)]
pub use edit::*;
pub use query::query_intersecting;

use crate::notes::{GroupId, LongNoteData};
use crate::rhythm::{BeatTime, ClockTime, ScrollPosition, Time, TrackId};

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

    /// Time of the note.
    fn time(&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>(
        time: 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.
    ///
    /// This method is only called when the note value is updated (via adding or replacing notes),
    /// or when the storage is rebuilt.
    fn apply_event_track(
        _note: crate::notes::NoteMut<'_, Self>,
        _tempo_track: &crate::rhythm::TempoTrack,
        _scroll_track: &crate::rhythm::ScrollSpeedTrack,
    ) where
        Self: Sized,
    {
    }
}

impl<N: ShortNoteData> LongNoteData for N {
    type ValueData = <N as ShortNoteData>::ValueData;
    type RuntimeData = <N as ShortNoteData>::RuntimeData;

    fn start_time(&self) -> Time {
        ShortNoteData::time(self)
    }
    fn duration(&self) -> Time {
        match self.time() {
            Time::Clock(_) => Time::Clock(ClockTime::ZERO),
            Time::Beat(_) => Time::Beat(BeatTime::ZERO),
        }
    }
    fn group_id(&self) -> GroupId {
        ShortNoteData::group_id(self)
    }
    fn value_data(&self) -> Self::ValueData {
        ShortNoteData::value_data(self)
    }
    fn reconstruct<'a>(
        start_time: Time,
        _duration: Time,
        value_data: <Self::ValueData as Soars>::Ref<'a>,
        group_id: GroupId,
    ) -> Self {
        ShortNoteData::reconstruct(start_time, value_data, group_id)
    }

    fn apply_event_track(
        note: crate::notes::NoteMut<'_, Self>,
        tempo_track: &crate::rhythm::TempoTrack,
        scroll_track: &crate::rhythm::ScrollSpeedTrack,
    ) where
        Self: Sized,
    {
        ShortNoteData::apply_event_track(note, tempo_track, scroll_track);
    }
}

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

/// Storage for all short notes of the same note type.
///
/// # Remarks
/// Notes of each group are stored separately in their own [`ShortNoteGroup`] 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 sorted by time (useful
/// for filtering notes for judgement), and one by scroll position (useful for filtering notes
/// for rendering).
#[derive(Clone)]
pub struct ShortNoteTypeStorage<Note: ShortNoteData> {
    note_id_counter: u64,
    note_id_lookup: HashMap<u64, NoteLocation>,
    time_sorted_groups: Vec<ShortNoteGroup<Note>>,
    scroll_sorted_groups: Vec<ShortNoteGroup<Note>>,
}

impl<N: std::fmt::Debug + ShortNoteData> std::fmt::Debug for ShortNoteTypeStorage<N> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ShortNoteTypeStorage")
            .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()
    }
}

/// Complete note components for one group, sorted by clock time or scroll position.
#[derive(Clone)]
pub struct ShortNoteGroup<Note: ShortNoteData> {
    group_id: GroupId,

    note_id: Vec<u64>,
    time: Vec<Time>,
    clock_time: Vec<ClockTime>,
    scroll_position: Vec<ScrollPosition>,
    value_data: Soa<Note::ValueData>,
    runtime_data: Soa<Note::RuntimeData>,
}

impl<Note: ShortNoteData> std::fmt::Debug for ShortNoteGroup<Note> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("ShortNoteGroup")
            .field("note_id", &self.note_id)
            .field("group_id", &self.group_id)
            .field("time", &self.time)
            .field("clock_time", &self.clock_time)
            .field("scroll_position", &self.scroll_position)
            .finish()
    }
}

impl<Note: ShortNoteData> ShortNoteGroup<Note> {
    pub fn new(group_id: GroupId) -> Self {
        Self {
            group_id,
            note_id: vec![],
            scroll_position: vec![],
            time: vec![],
            clock_time: vec![],
            value_data: Default::default(),
            runtime_data: Default::default(),
        }
    }
}

impl<Note: ShortNoteData> ShortNoteTypeStorage<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()
    }
    /// Nonempty note groups with components sorted by clock time.
    #[inline]
    pub fn time_sorted_groups(&self) -> &[ShortNoteGroup<Note>] {
        &self.time_sorted_groups
    }
    /// The same nonempty groups in the same group order, with components sorted by scroll position.
    #[inline]
    pub fn scroll_sorted_groups(&self) -> &[ShortNoteGroup<Note>] {
        &self.scroll_sorted_groups
    }
}

impl<Note: ShortNoteData> ShortNoteGroup<Note> {
    /// The note group shared by all components in this group.
    pub fn group_id(&self) -> &GroupId {
        &self.group_id
    }
    /// Note ids aligned with the other component arrays.
    pub fn note_id(&self) -> &[u64] {
        &self.note_id
    }

    /// Times of notes in this storage.
    #[inline]
    pub fn time(&self) -> &[Time] {
        &self.time
    }
    /// Clock times of notes in this storage.
    #[inline]
    pub fn clock_time(&self) -> &[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) -> &[ScrollPosition] {
        &self.scroll_position
    }
}