vsrg 0.3.0

Data structures for vertical scrolling rhythm games
Documentation
use super::{GroupId, LongNoteData, Note, NoteState};
use crate::rhythm::{ClockTime, Time};

/// Read-only view of a stored note.
///
/// [`NoteRef::value_data`] exposes the serializable note value components, while
/// [`NoteRef::runtime_data`] exposes the runtime state components that support judgement and
/// rendering.
///
/// The timing and grouping info are copies, since they are immutable in storage.
pub struct NoteRef<'a, N: LongNoteData> {
    /// The note's id.
    pub id: u64,
    /// The start time of the note, either beat or clock time.
    pub start_time: Time,
    /// The duration of the note, either beat or clock time. Zero for short notes.
    pub duration: Time,
    /// The note's clock time. Duration is zero for short notes.
    pub clock_time: Interval<ClockTime>,
    /// The id of the group this note belongs to.
    pub group_id: GroupId,
    /// Reference to the note's stored value.
    pub value_data: <N::ValueData as soa_rs::Soars>::Ref<'a>,
    /// Reference to the note's runtime state.
    pub runtime_data: <N::RuntimeData as soa_rs::Soars>::Ref<'a>,
}

/// Mutable view of a stored note.
///
/// This is similar to [`NoteRef`] but exposes [`NoteMut::runtime_data`] as mutable reference.
/// [`NoteMut::value_data`] are read-only, so any edit to a note's value must be done via the
/// notes storage's update methods.
///
/// The timing and grouping info are copies, since they are immutable in storage.
pub struct NoteMut<'a, N: LongNoteData> {
    /// The note's id.
    pub id: u64,
    /// The start time of the note, either beat or clock time.
    pub start_time: Time,
    /// The duration of the note, either beat or clock time. Zero for short notes.
    pub duration: Time,
    /// The note's clock time. Duration is zero for short notes.
    pub clock_time: Interval<ClockTime>,
    /// The id of the group this note belongs to.
    pub group_id: GroupId,
    /// Reference to the note's stored value.
    pub value_data: <N::ValueData as soa_rs::Soars>::Ref<'a>,
    /// Mut reference to the note's runtime state.
    pub runtime_data: <N::RuntimeData as soa_rs::Soars>::RefMut<'a>,
}

impl<N: LongNoteData> NoteRef<'_, N> {
    /// Reconstruct an owned note and clone its runtime components from SoA storage.
    pub fn to_snapshot(&self) -> NoteSnapshot<N> {
        NoteSnapshot {
            id: self.id,
            note: N::reconstruct(
                self.start_time,
                self.duration,
                soa_rs::AsSoaRef::as_soa_ref(&self.value_data),
                self.group_id.clone(),
            ),
            runtime_data: soa_rs::SoaClone::soa_clone(soa_rs::AsSoaRef::as_soa_ref(
                &self.runtime_data,
            )),
            clock_time: self.clock_time,
        }
    }
}

impl<N: LongNoteData> NoteMut<'_, N> {
    /// Borrow a read-only view for the duration of this shared borrow.
    pub fn as_ref(&self) -> NoteRef<'_, N> {
        NoteRef {
            id: self.id,
            start_time: self.start_time,
            duration: self.duration,
            clock_time: self.clock_time,
            group_id: self.group_id.clone(),
            value_data: soa_rs::AsSoaRef::as_soa_ref(&self.value_data),
            runtime_data: soa_rs::AsSoaRef::as_soa_ref(&self.runtime_data),
        }
    }

    /// Reconstruct an owned note and clone its runtime components from SoA storage.
    pub fn to_snapshot(&self) -> NoteSnapshot<N> {
        NoteSnapshot {
            id: self.id,
            note: N::reconstruct(
                self.start_time,
                self.duration,
                soa_rs::AsSoaRef::as_soa_ref(&self.value_data),
                self.group_id.clone(),
            ),
            runtime_data: soa_rs::SoaClone::soa_clone(soa_rs::AsSoaRef::as_soa_ref(
                &self.runtime_data,
            )),
            clock_time: self.clock_time,
        }
    }
}

/// An owned snapshot of a stored note, including its runtime components.
/// Clock time is copied from storage and describes the note at capture time.
#[derive(Clone)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct NoteSnapshot<N: LongNoteData> {
    /// Storage id, unique within the note type identified by `note`.
    pub id: u64,
    /// The note content.
    pub note: N,
    /// Runtime state cloned from storage at capture time.
    pub runtime_data: N::RuntimeData,
    /// The clock time of the note. Duration is zero for short notes.
    pub clock_time: Interval<ClockTime>,
}

impl<N: LongNoteData + std::fmt::Debug> std::fmt::Debug for NoteSnapshot<N>
where
    N::RuntimeData: std::fmt::Debug,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("NoteSnapshot")
            .field("id", &self.id)
            .field("note", &self.note)
            .field("runtime_data", &self.runtime_data)
            .field("clock_time", &self.clock_time)
            .finish()
    }
}

impl<N: LongNoteData + PartialEq> PartialEq for NoteSnapshot<N>
where
    N::RuntimeData: PartialEq,
{
    fn eq(&self, other: &Self) -> bool {
        self.id == other.id
            && self.note == other.note
            && self.runtime_data == other.runtime_data
            && self.clock_time == other.clock_time
    }
}

impl<N: LongNoteData> Note for NoteSnapshot<N> {
    fn start_time(&self) -> Time {
        self.note.start_time()
    }
    fn duration(&self) -> Time {
        self.note.duration()
    }
    fn group_id(&self) -> &GroupId {
        self.note.group_id()
    }
}

impl<N: LongNoteData + Clone> NoteState for NoteSnapshot<N> {
    type Snapshot = Self;

    fn to_snapshot(&self) -> Self {
        self.clone()
    }

    fn id(&self) -> u64 {
        self.id
    }
    fn clock_time_interval(&self) -> Interval<ClockTime> {
        self.clock_time
    }
}

/// An inclusive interval.
#[derive(Clone, Copy, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Interval<T> {
    start: T,
    end: T,
}

impl<T> Interval<T> {
    /// Creates an interval for types implementing [PartialOrd].
    ///
    /// You can pass vales in any order, a does not have to be smaller than b.
    /// Equal endpoints are also allowed.
    ///
    /// Returns [`None`] if the endpoints are incomparable, such as NaN or mixed clock and beat times.
    /// Consider using [Self::new] for types implementing [Ord] where this is not possible.
    pub fn try_new(a: T, b: T) -> Option<Self>
    where
        T: PartialOrd,
    {
        match a.partial_cmp(&b) {
            Some(std::cmp::Ordering::Greater) => Some(Self { start: b, end: a }),
            Some(_) => Some(Self { start: a, end: b }),
            None => None,
        }
    }

    /// Creates an interval for types implementing [Ord].
    ///
    /// You can pass vales in any order, a does not have to be smaller than b.
    /// Equal endpoints are also allowed.
    pub fn new(a: T, b: T) -> Self
    where
        T: Ord,
    {
        if a <= b {
            Self { start: a, end: b }
        } else {
            Self { start: b, end: a }
        }
    }

    /// Returns the inclusive lower endpoint.
    pub fn start(&self) -> T
    where
        T: Copy,
    {
        self.start
    }

    /// Returns the inclusive upper endpoint.
    pub fn end(&self) -> T
    where
        T: Copy,
    {
        self.end
    }

    /// Checks whether two intervals have any common values.
    pub fn intersect(&self, other: &Interval<T>) -> bool
    where
        T: Ord,
    {
        self.start <= other.end && self.end >= other.start
    }

    // Checks whether this interval is a superset of another interval.
    pub fn contains(&self, other: &T) -> bool
    where
        T: Ord,
    {
        &self.start <= other && other <= &self.end
    }
}

// We do not provide default implementation for T: PartialOrd, because
// user's Default impl is not guaranteed to be pure.
impl<T: Default + Ord> Default for Interval<T> {
    fn default() -> Self {
        Self::new(T::default(), T::default())
    }
}

impl<T> std::fmt::Debug for Interval<T>
where
    T: std::fmt::Debug,
{
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(std::format_args!("[{:?}, {:?}]", self.start, self.end))
    }
}

#[cfg(test)]
mod tests {
    use super::Interval;
    use crate::rhythm::{BeatTime, ClockTime, Time};
    use proptest::prelude::*;

    proptest! {
        #[test]
        fn endpoint_order_does_not_change_membership(a: i32, b: i32, point: i32) {
            let interval = Interval::new(a, b);
            prop_assert!(interval.start() <= interval.end());
            prop_assert_eq!(interval, Interval::new(b, a));
            prop_assert_eq!(Some(interval), Interval::try_new(a, b));
            prop_assert!(interval.contains(&a));
            prop_assert!(interval.contains(&b));
            prop_assert_eq!(interval.contains(&point),
                (a <= point && point <= b) || (b <= point && point <= a));
        }
    }

    #[test]
    fn equal_endpoints_and_touching_intervals_are_inclusive() {
        let point = Interval::new(3, 3);
        assert!(point.contains(&3));
        assert!(point.intersect(&Interval::new(5, 3)));
        assert!(!point.intersect(&Interval::new(5, 4)));
        assert_eq!(Interval::<i32>::default(), Interval::new(0, 0));
    }

    #[test]
    fn time_endpoints_are_sorted_within_their_unit() {
        let interval =
            Interval::try_new(Time::Clock(ClockTime::ONE), Time::Clock(ClockTime::ZERO)).unwrap();
        assert_eq!(interval.start(), Time::Clock(ClockTime::ZERO));
        assert_eq!(interval.end(), Time::Clock(ClockTime::ONE));
    }

    #[test]
    fn mixed_time_units_are_rejected() {
        assert_eq!(
            Interval::try_new(Time::Clock(ClockTime::ZERO), Time::Beat(BeatTime::ZERO)),
            None
        );
    }

    #[test]
    fn nan_endpoint_is_rejected() {
        assert!(Interval::try_new(0.0, f64::NAN).is_none());
        assert!(Interval::try_new(f64::NAN, 0.0).is_none());
        assert!(Interval::try_new(f64::NAN, f64::NAN).is_none());
    }
}