1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
//! # vsrg: Data structures for vertical scrolling rhythm games
//!
//! This crate provides the building blocks that lets you quickly create vertical scrolling rhythm
//! gameplay (hence vsrg) and its editor. Namely:
//! - [`rhythm`]: represent beats with [`BeatTime`], seconds with [`ClockTime`], and scrolling
//! coordinates with [`ScrollPosition`]. [`rhythm::TempoTrack`] convert between beats and seconds
//! [`rhythm::ScrollSpeedTrack`] convert clock time to scroll position. Both support eased changes.
//! - [`notes`]: store short and long notes in a struct-of-arrays layout, indexed by both the
//! note's clock time and scroll position, which means querying visible notes for rendering and
//! querying eligible notes for judgement is fast.
//! - [`generate_notes_storage!`] is a macro that generates storage for a game's note types with
//! ergonomic querying and editing methods.
//! - [`notes::relations`]: maintain relationships during edits, such as chords, matching
//! endpoints, parent-child links, and chains of notes.
//! - [`math`]: general-purpose math utilities, including [`math::Fraction`] and easing
//! curves used by [`rhythm`]. [`Easing`] provides monotonic curves; [`math::AdvancedEasing`]
//! adds curves that may be non-monotonic, with a more limited set of supported operations.
//!
//! # Helper modules
//!
//! - [`collections`]: general-purpose data structures, such as [`collections::IntervalTree`]. Used internally by [`notes`].
//!
//! # Optional features
//!
//! - `glam`: implement [`math::Easable`] for supported [`glam`] vector types.
//! - `serde`: enable serialization and deserialization of the easing enums.
//! - `lut_integration`: enable integration of [`math::AdvancedEasing`] curves using bundled lookup
//! tables, at the cost of bundling the tables in static data (about 450kb).
//!
//! # Getting started
//!
//! You do not have to use all functionalities of this crate, the [`math`] and [`rhythm`] modules are
//! designed to be usable in isolation. For more details on them, check the respective modules'
//! documentations.
//!
//! The rest of this section will guide you through how to quickly scaffold your rhythm game with
//! this crate. This is a birds' eye view that will gloss over many details. A full example is available
//! on the git repository if you need more details.
//!
//! # Examples
//! ```
//! use vsrg::{generate_notes_storage, ClockTime, Easing, Tempo, Time};
//! use vsrg::notes::{
//! query_intersecting, GroupId, Interval, LongNoteData, LongNoteTypeStorage,
//! NoteGroup, NoteGroups, NotesStorage, ShortNoteData, ShortNoteTypeStorage,
//! };
//! use vsrg::rhythm::{
//! ScrollSpeedChangeEvent, ScrollSpeedTrack, ScrollSpeedTracks,
//! ScrollPosition, TempoChangeEvent, TempoTrack, TempoTracks, TrackId,
//! };
//! use soa_rs::{SoaClone, Soars};
//!
//! // 1. Define tap and hold notes for a 4K rhythm game.
//! #[derive(Debug, Clone)]
//! struct TapNote {
//! // Clock time in this example; other games can store BeatTime or Time instead.
//! time: ClockTime,
//! lane: u16, // 0 to 3
//! }
//!
//! #[derive(Debug, Clone)]
//! struct HoldNote {
//! time: ClockTime,
//! duration: ClockTime,
//! lane: u16,
//! }
//!
//! // Runtime state supports judgement and rendering, and starts at Default::default().
//! // Both note types share these components in this example, but they can be different types.
//! #[derive(Clone, Soars, SoaClone, Default)]
//! struct NoteState {
//! completed: bool,
//! highlighted: bool,
//! }
//!
//! // Stored note values other than timing and group membership.
//! // Both note types share these components in this example, but they can be different types.
//! #[derive(Clone, Soars, SoaClone)]
//! struct NoteValue {
//! lane: u16,
//! }
//!
//! impl ShortNoteData for TapNote {
//! type ValueData = NoteValue;
//! type RuntimeData = NoteState;
//!
//! fn time(&self) -> Time { Time::Clock(self.time) }
//! // Main group only in this example, but you can store the concrete group id in the note.
//! fn group_id(&self) -> &GroupId { &GroupId::Main }
//! fn value_data(&self) -> NoteValue { NoteValue { lane: self.lane } }
//!
//! fn reconstruct<'a>(time: Time, data: <NoteValue as Soars>::Ref<'a>, group: GroupId) -> Self {
//! // This example stores only clock-based notes in the main group.
//! assert_eq!(group, GroupId::Main);
//! let Time::Clock(time) = time else { panic!("expected clock time") };
//! Self { time, lane: *data.lane }
//! }
//! }
//!
//! impl LongNoteData for HoldNote {
//! type ValueData = NoteValue;
//! type RuntimeData = NoteState;
//!
//! fn start_time(&self) -> Time { Time::Clock(self.time) }
//! fn duration(&self) -> Time { Time::Clock(self.duration) }
//! fn group_id(&self) -> &GroupId { &GroupId::Main }
//! fn value_data(&self) -> NoteValue { NoteValue { lane: self.lane } }
//!
//! fn reconstruct<'a>(
//! time: Time, duration: Time, data: <NoteValue as Soars>::Ref<'a>, group: GroupId,
//! ) -> Self {
//! assert_eq!(group, GroupId::Main);
//! let (Time::Clock(time), Time::Clock(duration)) = (time, duration) else {
//! panic!("expected clock time and duration")
//! };
//! Self { time, duration, lane: *data.lane }
//! }
//! }
//!
//! // 2. Generate storage and enums for all note types.
//! generate_notes_storage! {
//! enum MyNoteType,
//! enum MyNote,
//! enum MyNoteSnapshot,
//! enum MyNoteRef,
//! enum MyNoteMut,
//! struct My4kNotesStorage {
//! note_type Tap(TapNote) => taps: ShortNoteTypeStorage<TapNote>,
//! note_type Hold(HoldNote) => holds: LongNoteTypeStorage<HoldNote>,
//! }
//! }
//!
//! // 3. Define the note groups and their event tracks.
//! // Collections always have a main track. Named tracks can be added for other groups.
//! struct My4kEventTracks {
//! tempo_tracks: TempoTracks,
//! scroll_tracks: ScrollSpeedTracks,
//! }
//!
//! struct MyNoteGroup {
//! tempo_track: TrackId,
//! scroll_track: TrackId,
//! // The above two are the bare minimum, if your games requires other properties per note
//! // group, add them here.
//! }
//! impl NoteGroup for MyNoteGroup {
//! fn main_group() -> Self {
//! Self {
//! tempo_track: TrackId::Main,
//! scroll_track: TrackId::Main,
//! }
//! }
//! fn scroll_track_id(&self) -> &TrackId { &self.scroll_track }
//! fn tempo_track_id(&self) -> &TrackId { &self.tempo_track }
//! }
//!
//! // Keep groups and tracks available for later chart edits and rendering.
//! struct Chart {
//! notes: My4kNotesStorage,
//! groups: NoteGroups<MyNoteGroup>,
//! tracks: My4kEventTracks,
//! }
//!
//! // 4. Load the chart, for example after deserializing your chart format.
//! fn load_chart(tap_notes: Vec<TapNote>, hold_notes: Vec<HoldNote>) -> Chart {
//! let tempo = TempoTrack::with_events(vec![TempoChangeEvent {
//! time: Time::Clock(ClockTime::ZERO),
//! tempo: Tempo::from_bpm(120.0).unwrap(),
//! ease: Easing::InConst,
//! }]).unwrap();
//! let mut tempo_tracks = TempoTracks::with_tracks(tempo, std::iter::empty());
//! let scroll = ScrollSpeedTrack::with_events(
//! TrackId::Main,
//! vec![ScrollSpeedChangeEvent::new(
//! Time::Clock(ClockTime::ZERO), 1.0, Easing::InConst,
//! ).unwrap()],
//! &tempo_tracks,
//! );
//! let mut scroll_tracks = ScrollSpeedTracks::with_tracks(scroll, std::iter::empty());
//! let mut groups = NoteGroups::new();
//! let values = tap_notes.into_iter().map(MyNote::Tap)
//! .chain(hold_notes.into_iter().map(MyNote::Hold)).collect();
//! let notes = My4kNotesStorage::with_notes(
//! values, &mut groups, &mut tempo_tracks, &mut scroll_tracks,
//! );
//! Chart { notes, groups, tracks: My4kEventTracks { tempo_tracks, scroll_tracks } }
//! }
//!
//! // 5. During gameplay loop:
//! // Query a judgement window in clock time.
//! // The game can use these candidates to choose which note a key press should hit.
//! fn judge_notes(notes: &My4kNotesStorage, current_time: ClockTime) {
//! let window = ClockTime::from_milis(100.0).unwrap();
//!
//! let notes_in_range = notes.notes_ref_overlapping_range(Interval::new(
//! current_time - window, current_time + window,
//! ));
//!
//! for _note in notes_in_range {
//! todo!("Perform judgement logic here");
//! }
//! }
//!
//! // Query the visible scroll range for each note type.
//! fn render_notes(chart: &Chart, current_time: ClockTime) {
//! // How long your visible range is
//! let render_distance = ScrollPosition::new(10.0).expect("Not NaN");
//!
//! for group in chart.notes.taps().scroll_sorted_groups() {
//! let properties = chart
//! .groups
//! .get_group_or_main(group.group_id());
//! let scroll_track = chart
//! .tracks
//! .scroll_tracks
//! .get_track_or_main(properties.scroll_track_id());
//! let current_pos = scroll_track.calculate_scroll_position(current_time);
//! let far_pos = current_pos + render_distance;
//! let visible = Interval::new(current_pos, far_pos);
//!
//! // Quickly query for notes in range
//! for i in query_intersecting(group.scroll_position(), visible) {
//! // Retrieve any data that you need
//! let _note_value = group.value_data().get(i).unwrap();
//! let _note_state = group.runtime_data().get(i).unwrap();
//! let _note_scroll_pos = group.scroll_position()[i];
//! todo!("Draw the note");
//! }
//! }
//!
//! for group in chart.notes.holds().scroll_sorted_groups() {
//! let properties = chart
//! .groups
//! .get_group_or_main(group.group_id());
//! let scroll_track = chart
//! .tracks
//! .scroll_tracks
//! .get_track_or_main(properties.scroll_track_id());
//! let current_pos = scroll_track.calculate_scroll_position(current_time);
//! let far_pos = current_pos + render_distance;
//! let visible = Interval::new(current_pos, far_pos);
//!
//! // Quickly query for notes in range
//! for i in group.query_scroll_intersecting(visible) {
//! // Retrieve any data that you need
//! let _note_value = group.value_data().get(i).unwrap();
//! let _note_state = group.runtime_data().get(i).unwrap();
//! let _note_scroll_pos = group.scroll_position()[i];
//! todo!("Draw the note");
//! }
//! }
//! }
//!
//! // 6. Modify the chart at runtime in your editor.
//! // Adding, removing, and replacing notes is supported, as well as operating in batches.
//! // Each note is identified by a u64 id.
//! fn editor(chart: &mut Chart, tap_id: u64) {
//! chart.notes.replace_note(
//! &mut chart.groups,
//! &mut chart.tracks.tempo_tracks,
//! &mut chart.tracks.scroll_tracks,
//! tap_id,
//! MyNote::Tap(TapNote { time: ClockTime::from_seconds(4.0).unwrap(), lane: 3 }),
//! );
//! }
//! ```
//!
//! # Other tips
//! - vsrg uses float for beat time, which may be imprecise. You may want to store beat time as
//! [`math::Fraction`] in your game instead.
//!
//! - During hot loop, prefer querying the individual storage of each note type directly, similar
//! to `render_notes` in the above example. It both avoids cloning, allocating and has better
//! memory access pattern.
//!
//! - Avoid storing Vec or similar data structure in the note's value data and runtime data since
//! cloning them is expensive. Prefer Arc<[T]>, or even Rc<[T]> if your game logic is single
//! threaded.
//!
//! - [BeatTime], [ClockTime], [Tempo], [ScrollPosition] and other types are newtypes of [f64], and
//! have strict invariants (must be non NaN, infinities are allowed) to allow for [Ord]
//! implementation. Any time a math operation produces NaN, it instead returns [f64::INFINITY]
//! with the expectation that downstream code will safely ignore them. This should not be a
//! problem in most rhythm games, but is something you may want to keep in mind.
pub use generativity;
pub use soa_rs;
pub use Easing;
pub use ;