1use std::fmt::Display;
2
3#[derive(thiserror::Error, Debug)]
5pub enum Error {
6 #[error("Can't open the file!")]
8 CantOpenFile {
9 source: std::io::Error
11 },
12 #[error("The file isn't a CNM Audio Definition file.")]
14 NotCnmaFile,
15 #[error("Cnma file has a corrupt entry at line {0}!")]
17 CorruptedEntry(usize),
18 #[error("Cnma file has an entry without a mode!")]
20 NoMode,
21 #[error("Cnma file is corrupted because of {0}!")]
23 Corrupted(String),
24}
25
26#[derive(Debug, Clone, PartialEq)]
29pub struct ResourceId {
30 pub id: u32,
32 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#[derive(Debug, Default, Clone, PartialEq)]
65pub struct MaxPowerDef {
66 pub id: u8,
68 pub speed: f32,
70 pub jump: f32,
72 pub gravity: f32,
74 pub hpcost: f32,
76 pub strength: f32,
78 pub ability: Option<MaxPowerAbility>,
80}
81
82#[derive(Debug, Default, Clone, PartialEq)]
84pub enum MaxPowerAbility {
85 #[default]
87 DoubleJump,
88 Flying,
90 DropShield,
93 MarioBounce,
95}
96
97#[derive(Debug, Clone, PartialEq)]
99pub enum Mode {
100 MusicIds(Vec<ResourceId>),
102 SoundIds(Vec<ResourceId>),
104 MusicVolumeOverride,
106 LevelSelectOrder(Vec<String>),
109 MaxPowerDef(MaxPowerDef),
111 LuaAutorunCode(String),
114}
115
116#[derive(Debug)]
123pub struct Cnma {
124 pub modes: Vec<Mode>,
126}
127
128impl Cnma {
129 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 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 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}