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;