second_music_system/data.rs
1//! The "cold" parts of the Second Music System. Dead, immutable data.
2
3use super::*;
4
5use std::{borrow::Cow, collections::HashSet, str::FromStr, sync::OnceLock};
6
7mod parse;
8
9// ASCII printable non-digit non-letter characters (excluding underscore and
10// space and quote marks), and also throw in the funky inequality chars
11pub const EXPRESSION_SPLIT_CHARS: &str = r"!#$%&()*+,-./:;<=>?[\]^{|}~`@≤≥≠";
12
13/// A string, or a number.
14#[derive(Debug, Clone, PartialEq)]
15pub enum StringOrNumber {
16 String(CompactString),
17 Number(f32),
18}
19
20impl StringOrNumber {
21 /// When interpreting this as a boolean, it is true if:
22 /// - String: not empty, not equal to "0", not equal to "false"
23 /// - Number: not equal to zero (this means NaN is true)
24 pub fn is_truthy(&self) -> bool {
25 match self {
26 StringOrNumber::String(s) => {
27 !s.is_empty() && s.as_str() != "0" && s.as_str() != "false"
28 }
29 StringOrNumber::Number(n) => *n != 0.0,
30 }
31 }
32 /// When interpreting this is a number:
33 /// - Empty string: zero
34 /// - String that is a valid number: that number
35 /// - String that is an invalid number: NaN
36 /// - Any number: that number
37 pub fn as_number(&self) -> f32 {
38 match self {
39 StringOrNumber::String(s) => {
40 if s.is_empty() {
41 0.0
42 } else {
43 s.parse().unwrap_or(std::f32::NAN)
44 }
45 }
46 StringOrNumber::Number(n) => *n,
47 }
48 }
49 /// When interpreting this as a string:
50 /// - Any string: that string
51 /// - Any number: that number, rendered with default formatting, as a string
52 pub fn as_string(&self) -> Cow<str> {
53 match self {
54 StringOrNumber::String(s) => Cow::from(s),
55 StringOrNumber::Number(n) => Cow::from(format!("{}", n)),
56 }
57 }
58}
59
60impl Default for StringOrNumber {
61 fn default() -> StringOrNumber {
62 StringOrNumber::String(CompactString::new(""))
63 }
64}
65
66impl From<String> for StringOrNumber {
67 fn from(string: String) -> StringOrNumber {
68 StringOrNumber::String(string.into())
69 }
70}
71
72impl From<CompactString> for StringOrNumber {
73 fn from(string: CompactString) -> StringOrNumber {
74 StringOrNumber::String(string)
75 }
76}
77
78impl From<f32> for StringOrNumber {
79 fn from(f: f32) -> StringOrNumber {
80 StringOrNumber::Number(f)
81 }
82}
83
84impl From<bool> for StringOrNumber {
85 fn from(b: bool) -> StringOrNumber {
86 StringOrNumber::Number(if b { 1.0 } else { 0.0 })
87 }
88}
89
90impl FromStr for StringOrNumber {
91 type Err = String;
92 fn from_str(i: &str) -> Result<StringOrNumber, String> {
93 if let Ok(x) = i.parse() {
94 Ok(StringOrNumber::Number(x))
95 } else if let Some(x) = i.find(|x| EXPRESSION_SPLIT_CHARS.contains(x))
96 {
97 Err(format!(
98 "character {:?} is not allowed in a flow control string",
99 x
100 ))
101 } else {
102 Ok(StringOrNumber::String(i.to_compact_string()))
103 }
104 }
105}
106
107impl PartialOrd<StringOrNumber> for StringOrNumber {
108 fn partial_cmp(&self, rhs: &Self) -> Option<Ordering> {
109 match (self, rhs) {
110 (StringOrNumber::String(lhs), StringOrNumber::String(rhs)) => {
111 lhs.partial_cmp(rhs)
112 }
113 (StringOrNumber::Number(lhs), StringOrNumber::Number(rhs)) => {
114 lhs.partial_cmp(rhs)
115 }
116 _ => None,
117 }
118 }
119}
120
121#[derive(Debug, PartialEq)]
122pub(crate) struct Sound {
123 // All times are in seconds
124 // unique within a soundtrack
125 pub(crate) name: CompactString,
126 pub(crate) path: CompactString,
127 pub(crate) start: PosFloat,
128 // Will either be set in the soundtrack or backfilled after load.
129 pub(crate) end: OnceLock<PosFloat>,
130 /// If true, the underlying audio file should be streamed, rather than
131 /// cached. (If some sounds request that it be streamed and others request
132 /// that it be cached, whether it is streamed or cached is undefined.)
133 pub(crate) stream: bool,
134}
135
136impl Sound {
137 pub(crate) fn get_end(&self, delegate: &dyn SoundDelegate) -> PosFloat {
138 *self.end.get_or_init(|| {
139 delegate.warning(&format!("The length of sound {:?} is needed, but was not specified in the soundtrack, and could not be retrieved because the sound is marked for streaming. Set the length manually or disable streaming.", self.name));
140 PosFloat::ONE
141 })
142 }
143}
144
145#[derive(Debug, PartialEq)]
146pub(crate) enum SequenceElement {
147 PlaySound {
148 sound: CompactString,
149 channel: CompactString, // default is `main`
150 /// How many seconds of fade-in between starting and becoming full
151 /// volume
152 fade_in: PosFloat,
153 /// How long, including the fade in, that it should play at full volume
154 length: Option<PosFloat>,
155 /// How many seconds of fade-out to have after `length`
156 /// (whereas in the format, this is how long before `end` that the fade
157 /// will *start*)
158 fade_out: PosFloat,
159 },
160 PlaySequence {
161 sequence: CompactString,
162 },
163}
164
165#[derive(Debug, PartialEq)]
166pub(crate) struct Sequence {
167 // unique within a soundtrack
168 pub(crate) name: CompactString,
169 pub(crate) length: PosFloat,
170 pub(crate) elements: Vec<(PosFloat, SequenceElement)>,
171}
172
173impl Sequence {
174 /// Call the given handlers at least once with every sound or sequence
175 /// directly used by this Sequence.
176 pub fn find_all_direct_dependencies<A, B>(
177 &self,
178 mut found_sound: A,
179 mut found_sequence: B,
180 ) where
181 A: FnMut(&str),
182 B: FnMut(&str),
183 {
184 for (_time, element) in self.elements.iter() {
185 match element {
186 SequenceElement::PlaySound { sound, .. } => found_sound(sound),
187 SequenceElement::PlaySequence { sequence } => {
188 found_sequence(sequence)
189 }
190 }
191 }
192 }
193}
194
195#[derive(Debug, PartialEq)]
196pub(crate) enum Command {
197 /// Conclude the current node without running any more commands.
198 Done,
199 /// Wait a certain number of seconds.
200 Wait(PosFloat),
201 /// Start a Sound playing (even if another instance of that sound is
202 /// already playing)
203 PlaySound(CompactString),
204 /// Acts like `PlaySound` followed by `Wait`, but the amount of waiting
205 /// depends on the length of the named sound (information about which may
206 /// not be available at parse time).
207 PlaySoundAndWait(CompactString),
208 /// Start a Sequence playing (even if another instance of that sequence
209 /// is already playing)
210 PlaySequence(CompactString),
211 /// Acts like `PlaySequence` followed by `Wait`, but the amount of waiting
212 /// depends on the length of the named sequence (information about which
213 /// may not be available at parse time).
214 PlaySequenceAndWait(CompactString),
215 /// Cause another Node to start in parallel (iff not already playing)
216 StartNode(CompactString),
217 /// Cause another Node to start in parallel (iff not already playing), or
218 /// suddenly restart from the beginning (iff already playing)
219 RestartNode(CompactString),
220 /// As `RestartNode(the starting node)`
221 RestartFlow,
222 /// Change a FlowControl to a new value.
223 Set(CompactString, Vec<PredicateOp>),
224 /// If/else chain. **INTERMEDIATE PARSING STEP ONLY, MUST NOT OCCUR IN THE
225 /// FINAL DATA**
226 If {
227 /// Conditions to check, and commands to run if they're true.
228 branches: Vec<(Vec<PredicateOp>, Vec<Command>)>,
229 /// The commands to run if the condition evaluates to false.
230 fallback_branch: Vec<Command>,
231 },
232 /// Goto. If condition matches bool, jump to index. (Empty condition is
233 /// always true.)
234 Goto(Vec<PredicateOp>, bool, usize),
235 /// Placeholder where a Goto is about to go. **INTERMEDIATE PARSING STEP
236 /// ONLY, MUST NOT OCCUR IN THE FINAL DATA**
237 Placeholder,
238}
239
240#[derive(Debug, PartialEq)]
241pub(crate) struct Node {
242 pub(crate) name: Option<CompactString>,
243 pub(crate) commands: Vec<Command>,
244}
245
246impl Node {
247 pub fn new() -> Node {
248 Node {
249 name: None,
250 commands: vec![],
251 }
252 }
253}
254
255#[derive(Debug, PartialEq)]
256pub(crate) struct Flow {
257 // unique within a soundtrack
258 pub(crate) name: CompactString,
259 pub(crate) start_node: Arc<Node>,
260 pub(crate) nodes: HashMap<CompactString, Arc<Node>>,
261 pub(crate) autoloop: bool,
262}
263
264impl Flow {
265 /// Call the given handlers at least once with every Sound or Sequence
266 /// directly used by this Flow.
267 pub fn find_all_direct_dependencies<A, B>(
268 &self,
269 mut found_sound: A,
270 mut found_sequence: B,
271 ) where
272 A: FnMut(&str),
273 B: FnMut(&str),
274 {
275 for node in Some(&self.start_node)
276 .into_iter()
277 .chain(self.nodes.values())
278 {
279 for command in node.commands.iter() {
280 use Command::*;
281 match command {
282 PlaySound(x) | PlaySoundAndWait(x) => {
283 found_sound(x)
284 },
285 PlaySequence(x) | PlaySequenceAndWait(x) => {
286 found_sequence(x)
287 },
288 If { .. } => unreachable!("Command::If should not ever be in the final commands array, but was found"),
289 Placeholder => unreachable!("Command::Placeholder should not ever be in the final commands array, but was found"),
290 _ => (),
291 }
292 }
293 }
294 }
295 /// Return a Vec containing every Sound used by this Flow, directly or
296 /// indirectly. Calls the `missing_sound` and `missing_sequence` functions
297 /// exactly once for each sound or sequence that is referred to, but not
298 /// (currently) present within the Soundtrack.
299 pub fn find_all_sounds<A, B>(
300 &self,
301 soundtrack: &Soundtrack,
302 mut missing_sound: A,
303 mut missing_sequence: B,
304 ) -> Vec<Arc<Sound>>
305 where
306 A: FnMut(&str),
307 B: FnMut(&str),
308 {
309 let mut found_sounds = HashSet::new();
310 let mut found_sequences = HashSet::new();
311 let mut found_sound;
312 let mut found_sequence;
313 let mut indirects = Vec::with_capacity(soundtrack.sequences.len());
314 found_sound = |sound_name: &str| {
315 if !found_sounds.contains(sound_name) {
316 found_sounds.insert(sound_name.to_compact_string());
317 if !soundtrack.sounds.contains_key(sound_name) {
318 missing_sound(sound_name);
319 }
320 }
321 };
322 found_sequence = |sequence_name: &str| {
323 if !found_sequences.contains(sequence_name) {
324 found_sequences.insert(sequence_name.to_compact_string());
325 indirects.push(sequence_name.to_compact_string());
326 if !soundtrack.sequences.contains_key(sequence_name) {
327 missing_sequence(sequence_name);
328 }
329 }
330 };
331 self.find_all_direct_dependencies(
332 &mut found_sound,
333 &mut found_sequence,
334 );
335 let mut n = 0;
336 while n < indirects.len() {
337 if let Some(sequence) = soundtrack.sequences.get(&indirects[n]) {
338 let mut found_sequence = |sequence_name: &str| {
339 if !found_sequences.contains(sequence_name) {
340 found_sequences
341 .insert(sequence_name.to_compact_string());
342 indirects.push(sequence_name.to_compact_string());
343 if !soundtrack.sequences.contains_key(sequence_name) {
344 missing_sequence(sequence_name);
345 }
346 }
347 };
348 sequence.find_all_direct_dependencies(
349 &mut found_sound,
350 &mut found_sequence,
351 )
352 }
353 n += 1;
354 }
355 found_sounds
356 .into_iter()
357 .filter_map(|k| soundtrack.sounds.get(&k).cloned())
358 .collect()
359 }
360}
361
362#[derive(Debug, PartialEq)]
363pub(crate) enum PredicateOp {
364 /// Push the value of the given `FlowControl`, empty string if unset.
365 PushVar(CompactString),
366 /// Push the given value.
367 PushConst(StringOrNumber),
368 /// Pop two elements, push whether they're equal.
369 Eq,
370 /// Pop two elements, push whether they're unequal.
371 NotEq,
372 /// Pop two elements, push whether the second from the top is greater than
373 /// the top.
374 Greater,
375 /// Pop two elements, push whether the second from the top is greater than
376 /// or equal to the top.
377 GreaterEq,
378 /// Pop two elements, push whether the second from the top is lesser than
379 /// the top.
380 Lesser,
381 /// Pop two elements, push whether the second from the top is lesser than
382 /// or equal to the top.
383 LesserEq,
384 /// Pop two elements, push whether they're both truthy.
385 And,
386 /// Pop two elements, push whether at least one is truthy.
387 Or,
388 /// Pop two elements, push whether only one is truthy.
389 Xor,
390 /// Pop one element, push whether it's not truthy.
391 Not,
392 /// Pop two elements, push their numeric sum.
393 Add,
394 /// Pop two elements, push their numeric difference.
395 Sub,
396 /// Pop two elements, push their numeric prodict.
397 Mul,
398 /// Pop two elements, push their numeric quotient.
399 Div,
400 /// Pop two elements, push their numeric remainder. (As Lua % operator)
401 Rem,
402 /// Pop two elements, push the floor of their numeric quotient.
403 IDiv,
404 /// Pop two elements, push the result of exponentiation.
405 Pow,
406 /// Pop one element, return its sine (as degrees).
407 Sin,
408 /// Pop one element, return its cosine (as degrees).
409 Cos,
410 /// Pop one element, return its tangent (as degrees).
411 Tan,
412 /// Pop one element, return its arcsine (in degrees).
413 ASin,
414 /// Pop one element, return its arccosine (in degrees).
415 ACos,
416 /// Pop one element, return its arctangent (in degrees).
417 ATan,
418 /// Pop two elements, return atan2 (in degrees).
419 ATan2,
420 /// Pop one element, return its natural logarithm.
421 Log,
422 /// Pop one element, return its natural exponent.
423 Exp,
424 /// Pop one element, return its floor.
425 Floor,
426 /// Pop one element, return its ceiling.
427 Ceil,
428 /// Pop two elements, return the one closer to negative infinity.
429 Min,
430 /// Pop two elements, return the one closer to positive infinity.
431 Max,
432 /// Pop one element, return its absolute value.
433 Abs,
434 /// Pop one element, push -1 if it's negative, 1 if it's positive.
435 Sign,
436 /// Pop one element, push its negation.
437 Negate,
438}
439
440#[cfg(test)]
441mod test;