cnmo_parse/
cnma.rs

1use std::fmt::Display;
2
3/// Errors when dealing with CNMA files
4#[derive(thiserror::Error, Debug)]
5pub enum Error {
6    /// [`from_file`](crate::cnma::Cnma::from_file) and [`save`](crate::cnma::Cnma::save) can return this
7    #[error("Can't open the file!")]
8    CantOpenFile {
9        /// The actuall [`std::io::Error`] from reading/writing to the file.
10        source: std::io::Error
11    },
12    /// The file is not a CNM config file.
13    #[error("The file isn't a CNM Audio Definition file.")]
14    NotCnmaFile,
15    /// There was a corrupted entry at the line specified
16    #[error("Cnma file has a corrupt entry at line {0}!")]
17    CorruptedEntry(usize),
18    /// There was an mode field but it had no mode name
19    #[error("Cnma file has an entry without a mode!")]
20    NoMode,
21    /// The file is corrupted because of the string inside of the tuple variant
22    #[error("Cnma file is corrupted because of {0}!")]
23    Corrupted(String),
24}
25
26/// Used in the SoundID and the MusicID modes to specify what file a sound
27/// is related to and its related ID.
28#[derive(Debug, Clone, PartialEq)]
29pub struct ResourceId {
30    /// The ID for CNM Online. If 2 are the same, the last is used and a memory leak occurs.
31    pub id: u32,
32    /// The path to the file in question. It is relative based on the exe's directory and
33    /// doesn't need a "./" at the start.
34    pub path: String,
35}
36
37impl Display for ResourceId {
38    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
39        f.write_fmt(format_args!("Id: {}, Path: {}", self.id, self.path))
40    }
41}
42
43impl ResourceId {
44    fn from_line(line_num: usize, line: &str) -> Result<Self, Error> {
45        Ok(ResourceId {
46            id: match line.split_whitespace().nth(0) {
47                Some(id) => match id.parse() {
48                    Ok(id) => id,
49                    Err(_) => return Err(Error::CorruptedEntry(line_num + 1)),
50                },
51                None => return Err(Error::CorruptedEntry(line_num + 1)),
52            },
53            path: match line.split_whitespace().nth(1) {
54                Some(path) => path.to_string(),
55                None => return Err(Error::CorruptedEntry(line_num + 1)),
56            }
57        })
58    }
59}
60
61/// A power defintion for a particular skin id, or any id.
62/// These are activated when hitting the MaxPower trigger in game
63/// and modify your player stats.
64#[derive(Debug, Default, Clone, PartialEq)]
65pub struct MaxPowerDef {
66    /// The skin id for this power definition
67    pub id: u8,
68    /// The speed multiplier
69    pub speed: f32,
70    /// Jump multiplier
71    pub jump: f32,
72    /// Gravity multiplier
73    pub gravity: f32,
74    /// How many hp points are removed per second while the power is active
75    pub hpcost: f32,
76    /// The strength multiplier
77    pub strength: f32,
78    /// An optional double jump ability.
79    pub ability: Option<MaxPowerAbility>,
80}
81
82/// A ability activated on a double jump.
83#[derive(Debug, Default, Clone, PartialEq)]
84pub enum MaxPowerAbility {
85    /// Basic double jump
86    #[default]
87    DoubleJump,
88    /// Basic flying ability akin to the normal wings power up
89    Flying,
90    /// Activates a sheild once you hit the ground for a short moment
91    /// of time
92    DropShield,
93    /// Allows you to bounce on enemies but not hurt them.
94    MarioBounce,
95}
96
97/// What section/mode contents there are
98#[derive(Debug, Clone, PartialEq)]
99pub enum Mode {
100    /// Music Resource Ids
101    MusicIds(Vec<ResourceId>),
102    /// Sound Resource Ids
103    SoundIds(Vec<ResourceId>),
104    /// A deprecated CNM Online section
105    MusicVolumeOverride,
106    /// The order of levels on the level select menu, same order
107    /// as the strings in the vector here.
108    LevelSelectOrder(Vec<String>),
109    /// A power defintion for a particular skin
110    MaxPowerDef(MaxPowerDef),
111    /// Code run at the beginning of the game, certain function names
112    /// will run as hooks for object code, etc.
113    LuaAutorunCode(String),
114}
115
116/// CNMA file. Holds generic configuration of the game and resource
117/// locations.
118/// - Sound and music ids and file paths
119/// - Lua scripting code
120/// - Custom upgrade/powers
121/// - The order of the level select menu
122#[derive(Debug)]
123pub struct Cnma {
124    /// Vector of the sections of the file.
125    pub modes: Vec<Mode>,
126}
127
128impl Cnma {
129    /// Load a Cnma config from a file
130    pub fn from_file<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
131        let s = match std::fs::read_to_string(path) {
132            Ok(s) => s,
133            Err(e) => return Err(Error::CantOpenFile { source: e }),
134        };
135        Self::from_string(s.as_str())
136    }
137
138    /// Load a Cnma config from a string
139    pub fn from_string(s: &str) -> Result<Self, Error> {
140        let mut cnma = Cnma { modes: Vec::new() };
141        let mut current_mode: Option<Mode> = None;
142        let mut mode_locked = false;
143
144        let append_mode = |cnma: &mut Cnma, current_mode: &mut Option<Mode>| {
145            if let Some(mode) = current_mode.as_mut() {
146                if let Mode::LevelSelectOrder(ref mut vec) = mode {
147                    vec.reverse();
148                }
149
150                cnma.modes.push(mode.clone());
151            }
152        };
153        
154        for (line_num, line) in s.lines().enumerate() {
155            if line.starts_with("MODE") && !mode_locked {
156                append_mode(&mut cnma, &mut current_mode);
157                current_mode = match line.split_whitespace().nth(1) {
158                    Some("MUSIC") => Some(Mode::MusicIds(Vec::new())),
159                    Some("SOUNDS") => Some(Mode::SoundIds(Vec::new())),
160                    Some("MUSIC_VOLUME_OVERRIDE") => Some(Mode::MusicVolumeOverride),
161                    Some("LEVELSELECT_ORDER") => Some(Mode::LevelSelectOrder(Vec::new())),
162                    Some(s) if s.starts_with("MAXPOWER") => Some(Mode::MaxPowerDef(MaxPowerDef {
163                        id: s[s.find(|c: char| c.is_digit(10)).unwrap_or_default()..s.len()].parse().unwrap_or_default(),
164                        ..Default::default()
165                    })),
166                    Some("LUA_AUTORUN") => {
167                        mode_locked = true;
168                        Some(Mode::LuaAutorunCode("".to_string()))
169                    },
170                    Some(mode_name) => return Err(Error::Corrupted(format!("Unkown audio mode name {mode_name}"))),
171                    None => return Err(Error::Corrupted(format!("Expected a mode name after \"MODE\" on line {}", line_num + 1))),
172                };
173
174                continue;
175            } else if line.starts_with("__ENDLUA__") && mode_locked {
176                match current_mode {
177                    Some(Mode::LuaAutorunCode(_)) => {
178                        mode_locked = false;
179                        cnma.modes.push(current_mode.as_ref().unwrap().clone());
180                        current_mode = None;
181                    },
182                    _ => return Err(Error::Corrupted("__ENDLUA__ found outside of LUA_AUTORUN mode segment!".to_string())),
183                }
184
185                continue;
186            }
187
188            match current_mode.as_mut() {
189                Some(&mut Mode::MusicIds(ref mut v)) => {
190                    v.push(ResourceId::from_line(line_num, line)?)
191                },
192                Some(&mut Mode::SoundIds(ref mut v)) => {
193                    v.push(ResourceId::from_line(line_num, line)?)
194                },
195                Some(&mut Mode::MusicVolumeOverride) => {},
196                Some(&mut Mode::LevelSelectOrder(ref mut v)) => {
197                    match line.split_whitespace().nth(0) {
198                        Some(s) => v.push(s.to_string()),
199                        None => return Err(Error::CorruptedEntry(line_num + 1)),
200                    }
201                },
202                Some(&mut Mode::MaxPowerDef(ref mut def)) => {
203                    let (field_name, field_value) = match (line.split_whitespace().nth(0), line.split_whitespace().nth(1)) {
204                        (Some(n), Some(v)) => (n, v),
205                        _ => return Err(Error::CorruptedEntry(line_num + 1)),
206                    };
207
208                    match field_name {
209                        "spd" => def.speed = field_value.parse().unwrap_or_default(),
210                        "jmp" => def.jump = field_value.parse().unwrap_or_default(),
211                        "grav" => def.gravity = field_value.parse().unwrap_or_default(),
212                        "hpcost" => def.hpcost = field_value.parse().unwrap_or_default(),
213                        "strength" => def.strength = field_value.parse().unwrap_or_default(),
214                        "ability" => def.ability = match field_value.parse().unwrap_or_default() {
215                            0 => None,
216                            1 => Some(MaxPowerAbility::DoubleJump),
217                            2 => Some(MaxPowerAbility::Flying),
218                            3 => Some(MaxPowerAbility::DropShield),
219                            4 => Some(MaxPowerAbility::MarioBounce),
220                            _ => return Err(Error::CorruptedEntry(line_num + 1)),
221                        },
222                        _ => return Err(Error::CorruptedEntry(line_num + 1)),
223                    };
224                },
225                Some(&mut Mode::LuaAutorunCode(ref mut code)) => {
226                    code.push_str((line.to_string() + "\n").as_str());
227                },
228                None => return Err(Error::NoMode),
229            }
230        }
231
232        append_mode(&mut cnma, &mut current_mode);
233
234        Ok(cnma)
235    }
236
237    /// Saves the cnma file to the path specified, creates the file if it doesn't
238    /// exist and overwrites it if it does.
239    pub fn save<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), Error> {
240        let mut contents = "".to_string();
241        
242        for mode in self.modes.iter() {
243            match mode {
244                &Mode::MusicIds(ref v) => {
245                    contents.push_str("MODE MUSIC\n");
246                    for res in v.iter() {
247                        contents.push_str(format!("{} {}\n", res.id, res.path).as_str());
248                    }
249                },
250                &Mode::SoundIds(ref v) => {
251                    contents.push_str("MODE SOUNDS\n");
252                    for res in v.iter() {
253                        contents.push_str(format!("{} {}\n", res.id, res.path).as_str());
254                    }
255                },
256                &Mode::MusicVolumeOverride => {
257                    contents.push_str("MODE MUSIC_VOLUME_OVERRIDE\n");
258                },
259                &Mode::LevelSelectOrder(ref v) => {
260                    contents.push_str("MODE LEVELSELECT_ORDER\n");
261                    for lvl in v.iter().rev() {
262                        contents.push_str(format!("{} _\n", lvl).as_str());
263                    }
264                },
265                &Mode::MaxPowerDef(ref def) => {
266                    let ability_id = match def.ability {
267                        None => 0,
268                        Some(MaxPowerAbility::DoubleJump) => 1,
269                        Some(MaxPowerAbility::Flying) => 2,
270                        Some(MaxPowerAbility::DropShield) => 3,
271                        Some(MaxPowerAbility::MarioBounce) => 4,
272                    };
273
274                    contents.push_str(format!("MODE MAXPOWER{}\n", def.id).as_str());
275                    contents.push_str(format!("spd {}\n", def.speed).as_str());
276                    contents.push_str(format!("jmp {}\n", def.jump).as_str());
277                    contents.push_str(format!("grav {}\n", def.gravity).as_str());
278                    contents.push_str(format!("hpcost {}\n", def.hpcost).as_str());
279                    contents.push_str(format!("strength {}\n", def.strength).as_str());
280                    contents.push_str(format!("ability {}\n", ability_id).as_str());
281                },
282                &Mode::LuaAutorunCode(ref s) => {
283                    contents.push_str("MODE LUA_AUTORUN\n");
284                    contents.push_str(s.as_str());
285                },
286            }
287
288            if let &Mode::LuaAutorunCode(_) = mode {
289                contents.push_str("__ENDLUA__\n");
290            }
291        }
292
293        match std::fs::write(path, contents) {
294            Err(e) => Err(Error::CantOpenFile { source: e }),
295            _ => Ok(()),
296        }
297    }
298}