Skip to main content

mxp/elements/
sound.rs

1use std::borrow::Cow;
2use std::fmt;
3use std::num::{NonZero, ParseIntError};
4use std::str::FromStr;
5
6use crate::arguments::{ArgumentScanner, ExpectArg as _};
7use crate::parse::Decoder;
8
9/// Specifies the number of times a sound/music file should be played.
10///
11/// See [MSP specification: L parameter](https://www.zuggsoft.com/zmud/msp.htm#MSP%20Specification).
12#[derive(Copy, Clone, Debug, PartialEq, Eq)]
13pub enum AudioRepetition {
14    /// The file should be played infinitely, until instructed otherwise.
15    Forever,
16    /// The file should play this many times.
17    Count(NonZero<u32>),
18}
19
20impl Default for AudioRepetition {
21    /// A single play, i.e. `AudioRepetition::Count(1)`.
22    fn default() -> Self {
23        Self::Count(NonZero::new(1).unwrap())
24    }
25}
26
27impl fmt::Display for AudioRepetition {
28    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
29        match self {
30            Self::Forever => (-1).fmt(f),
31            Self::Count(amount) => amount.fmt(f),
32        }
33    }
34}
35
36impl FromStr for AudioRepetition {
37    type Err = ParseIntError;
38
39    fn from_str(s: &str) -> Result<Self, Self::Err> {
40        if s == "-1" {
41            return Ok(Self::Forever);
42        }
43        s.parse().map(Self::Count)
44    }
45}
46
47/// Sound triggers are WAV format files intended for sound effects.
48///
49/// See [MXP specification: `<SOUND>`](https://www.zuggsoft.com/zmud/mxp.htm#MSP%20Compatibility)
50/// and the [MSP (Mud Sound Protocol) specification](https://www.zuggsoft.com/zmud/msp.htm).
51///
52/// # Examples
53///
54/// ```
55/// use mxp::AudioRepetition;
56///
57/// assert_eq!(
58///     "<SOUND 'weather/rain.wav' V=80 L=3 P=10 T=combat U='http://example.org:5000/sound'>".parse::<mxp::Sound>(),
59///     Ok(mxp::Sound {
60///         fname: "weather/rain.wav".into(),
61///         volume: 80,
62///         repeat: AudioRepetition::Count(3.try_into().unwrap()),
63///         priority: 10,
64///         class: Some("combat".into()),
65///         url: Some("http://example.org:5000/sound".into()),
66///     }),
67/// );
68/// ```
69#[derive(Copy, Clone, Debug, PartialEq, Eq)]
70pub struct Sound<S = String> {
71    /// File name. May contain wildcards. If no extension is specified, ".wav" should be assumed.
72    pub fname: S,
73    /// Volume between 0 and 100.
74    pub volume: u8,
75    /// Repeat behavior.
76    pub repeat: AudioRepetition,
77    /// This parameter applies when some sound is playing and another request arrives. Then, if new
78    /// request has higher (but NOT equal) priority than the one that's currently being played, old
79    /// sound must be stopped and the new sound starts playing instead. In the case of a tie, the
80    /// sound that is already playing wins.
81    pub priority: u8,
82    /// Type of sound, e.g. combat, zone, death, clan. Case-insensitive. This parameter was
83    /// intended to provide a way to group sounds into subfolders within the main sound directory.
84    pub class: Option<S>,
85    /// Specifies the URL of the sound file. This allows downloading files from the MUD server.
86    /// Client should always look in local directories first, and only download the file if it's
87    /// not available locally.
88    pub url: Option<S>,
89}
90
91impl<S: Default> Default for Sound<S> {
92    fn default() -> Self {
93        Self {
94            fname: S::default(),
95            volume: 100,
96            repeat: AudioRepetition::default(),
97            priority: 50,
98            class: None,
99            url: None,
100        }
101    }
102}
103
104impl<S: AsRef<str>> Sound<S> {
105    /// Returns `true` if this command is a `<SOUND OFF>` command, causing sounds to stop rather
106    /// than triggering a sound.
107    pub fn is_off(&self) -> bool {
108        self.fname.as_ref().eq_ignore_ascii_case("off") && self.url.is_none()
109    }
110}
111
112impl<S> Sound<S> {
113    /// Applies a type transformation to all text, returning a new struct.
114    pub fn map_text<T, F>(self, mut f: F) -> Sound<T>
115    where
116        F: FnMut(S) -> T,
117    {
118        Sound {
119            fname: f(self.fname),
120            volume: self.volume,
121            repeat: self.repeat,
122            priority: self.priority,
123            class: self.class.map(&mut f),
124            url: self.url.map(f),
125        }
126    }
127}
128
129impl_into_owned!(Sound);
130
131impl<S: AsRef<str>> Sound<S> {
132    /// Returns a new struct that borrows text from this one.
133    pub fn borrow_text(&self) -> Sound<&str> {
134        Sound {
135            fname: self.fname.as_ref(),
136            volume: self.volume,
137            repeat: self.repeat,
138            class: self.class.as_ref().map(AsRef::as_ref),
139            url: self.url.as_ref().map(AsRef::as_ref),
140            priority: self.priority,
141        }
142    }
143}
144
145impl_partial_eq!(Sound);
146
147impl<S: AsRef<str>> Sound<S> {
148    pub(crate) fn scan<A>(mut scanner: A) -> crate::Result<Self>
149    where
150        A: ArgumentScanner<Output = S>,
151    {
152        let fname = scanner.decode_next_or("fname")?.expect_some("fname")?;
153        let volume = scanner.decode_next_or("v")?.expect_number()?.unwrap_or(100);
154        let repeat = scanner
155            .decode_next_or("l")?
156            .expect_number()?
157            .unwrap_or_default();
158        let priority = scanner.decode_next_or("p")?.expect_number()?.unwrap_or(50);
159        let class = scanner.decode_next_or("t")?;
160        let url = scanner.decode_next_or("u")?;
161        scanner.expect_end()?;
162        Ok(Self {
163            fname,
164            volume,
165            repeat,
166            priority,
167            class,
168            url,
169        })
170    }
171}
172
173impl_from_str!(Sound);
174
175impl<S: AsRef<str>> fmt::Display for Sound<S> {
176    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
177        let Sound {
178            fname,
179            volume,
180            repeat,
181            priority,
182            class,
183            url,
184        } = self.borrow_text();
185        crate::display::ElementFormatter {
186            name: "SOUND",
187            arguments: &[
188                &fname,
189                &(volume, 100),
190                &(repeat, AudioRepetition::default()),
191                &(priority, 50),
192                &class,
193                &url,
194            ],
195            keywords: &[],
196        }
197        .fmt(f)
198    }
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204    use crate::test_utils::{StringPair, format_from_pairs, parse_from_pairs};
205
206    const AUDIO_REPETITION_PAIRS: &[StringPair<AudioRepetition>] = &[
207        (AudioRepetition::Forever, "-1"),
208        (AudioRepetition::Count(NonZero::new(10).unwrap()), "10"),
209    ];
210
211    #[test]
212    fn fmt_audio_repetition() {
213        let (actual, expected) = format_from_pairs(AUDIO_REPETITION_PAIRS);
214        assert_eq!(actual, expected);
215    }
216
217    #[test]
218    fn parse_audio_repetition() {
219        let (actual, expected) = parse_from_pairs(AUDIO_REPETITION_PAIRS);
220        assert_eq!(actual, expected);
221    }
222
223    #[test]
224    fn parse_audio_repetition_invalid() {
225        assert_eq!(
226            "0".parse::<AudioRepetition>(),
227            Err("0".parse::<NonZero<u32>>().unwrap_err())
228        );
229    }
230}