cnmo_parse/lparse/level_data/
mod.rs

1use crate::lparse::{
2    Error,
3    LParse
4};
5
6use self::cnmb_types::{BackgroundLayer, TileProperties};
7
8/// Types of the CNMB file.
9/// This includes tile properties, the world tiles, and backgrounds.
10pub mod cnmb_types;
11/// Types of the CNMS file.
12/// This includes world objects, strings from the world, world title, etc.
13pub mod cnms_types;
14/// Consts used in CNM Online that are also used here (like tile size).
15pub mod consts;
16
17/// Duration of something in ticks. (There are 30 ticks per second in CNM
18/// Online, so a Duration of 30 is 1 second). Negative values have uses in
19/// very specific and special cases in CNM Online. Mostly is 0 or above though.
20#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
21#[derive(Debug, Default, Copy, Clone)]
22pub struct Duration(pub i32);
23
24/// Defines a point for CNM types.
25#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
26#[derive(Debug, Default, Copy, Clone)]
27pub struct Point(pub f32, pub f32);
28
29/// Version specs of the level data (seperate from the lparse file version)
30#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
31#[derive(Debug)]
32pub struct VersionSpecs {
33    version: u32,
34    num_teleports: usize,
35    num_spawns: usize,
36    num_spawn_modes: usize,
37    teleport_name_size: usize,
38    max_tile_frames: usize,
39    ending_text_lines: usize,
40    ending_text_line_len: usize,
41    background_layers: usize,
42    title_ending_text_line: usize,
43    preview_tile_index: usize,
44}
45
46impl VersionSpecs {
47    /// Creates it from a specific level file version
48    /// Only current level format in CNM Online is ID 1.
49    pub fn from_version(version: u32) -> Result<Self, Error> {
50        match version {
51            1 => Ok(Self {
52                version,
53                num_teleports: 512,
54                num_spawns: 128,
55                num_spawn_modes: 3,
56                teleport_name_size: 41,
57                max_tile_frames: 32,
58                ending_text_lines: 48,
59                ending_text_line_len: 32,
60                background_layers: 32,
61                title_ending_text_line: 47,
62                preview_tile_index: 256,
63            }),
64            _ => Err(Error::UnknownVersion(version))
65        }
66    }
67
68    /// Version ID
69    pub fn get_version(&self) -> u32 {
70        self.version
71    }
72
73    /// Maximum number of teleports supported
74    pub fn get_num_teleports(&self) -> usize {
75        self.num_teleports
76    }
77    
78    /// Maximum number of spawns per mode (there are 2 modes used, 1 unused)
79    /// Those modes being, player spawns, checkpoints, and an unused mode.
80    pub fn get_num_spawns(&self) -> usize {
81        self.num_spawns
82    }
83
84    /// Number of spawner modes.
85    pub fn get_num_spawn_modes(&self) -> usize {
86        self.num_spawn_modes
87    }
88
89    /// Maximum size of a teleport name in bytes. All names in CNM Online are
90    /// ascii.
91    pub fn get_teleport_name_size(&self) -> usize {
92        self.teleport_name_size
93    }
94
95    /// Maximum amount of frames an animated tile can have in CNM Online
96    pub fn get_max_tile_frames(&self) -> usize {
97        self.max_tile_frames
98    }
99
100    /// Number of text lines saved in CNMS files.
101    pub fn get_ending_text_lines(&self) -> usize {
102        self.ending_text_lines
103    }
104
105    /// The length of each CNMS text line in bytes.
106    pub fn get_ending_text_line_len(&self) -> usize {
107        self.ending_text_line_len
108    }
109
110    /// The maximum amount of background layers this version supports.
111    pub fn get_background_layers(&self) -> usize {
112        self.background_layers
113    }
114    
115    /// What text line the title is on in this version
116    pub fn get_title_ending_text_line(&self) -> usize {
117        self.title_ending_text_line
118    }
119
120    /// What tile index controls the preview for a level in this version
121    pub fn get_preview_tile_index(&self) -> usize {
122        self.preview_tile_index
123    }
124}
125
126/// Difficulty rating for a level
127#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
128#[derive(Debug, PartialEq, Eq, num_derive::FromPrimitive, num_derive::ToPrimitive)]
129pub enum DifficultyRating {
130    ///
131    Tutorial,
132    ///
133    ReallyEasy,
134    ///
135    Easy,
136    ///
137    Normal,
138    ///
139    KindaHard,
140    ///
141    Hard,
142    ///
143    Ultra,
144    ///
145    Extreme,
146    ///
147    Dealth,
148    ///
149    UltraDeath,
150}
151
152impl DifficultyRating {
153    /// Creates a difficuly rating from an ID
154    pub fn from_difficulty_id(id: u8) -> Option<Self> {
155        num_traits::FromPrimitive::from_u8(id)
156    }
157
158    /// Gets the assiciated difficulty ID from this difficulty rating
159    pub fn get_difficulty_id(&self) -> u8 {
160        num_traits::ToPrimitive::to_u8(self).unwrap_or(3)
161    }
162
163    /// To a string with spaces
164    pub fn to_string_pretty(&self) -> String {
165        match self {
166            &Self::Tutorial => "Tutorial".to_string(),
167            &Self::ReallyEasy => "Really Easy".to_string(),
168            &Self::Easy => "Easy".to_string(),
169            &Self::Normal => "Normal".to_string(),
170            &Self::KindaHard => "Kinda Hard".to_string(),
171            &Self::Hard => "Hard".to_string(),
172            &Self::Ultra => "Ultra!".to_string(),
173            &Self::Extreme => "Extreme!".to_string(),
174            &Self::Dealth => "Death!!!".to_string(),
175            &Self::UltraDeath => "ULTRA DEATH!".to_string(),
176        }
177    }
178}
179
180/// Metadata for this level.
181/// 
182/// Controls stuff for how its shown on the level select menu.
183#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
184#[derive(Debug)]
185pub struct LevelMetaData {
186    /// Title of the level
187    pub title: String,
188    /// Subtitle appears after the normal title with a space and appears
189    /// under the title when the level start card appears
190    pub subtitle: Option<String>,
191    /// The location of the preview in tile coordinates on GFX.BMP
192    pub preview_loc: (u32, u32),
193    /// The difficulty rating of the level
194    pub difficulty_rating: DifficultyRating,
195}
196
197impl LevelMetaData {
198    /// Creates Level MetaData from an LParse file
199    pub fn from_lparse(cnmb: &LParse, cnms: &LParse, version: &VersionSpecs, ignore_warnings: bool) -> Result<Self, Error> {
200        let title_full = cnms_types::get_ending_text_line(cnms, version, version.title_ending_text_line)?;
201        let title = title_full.split('\\').next().unwrap_or("").to_string();
202        let subtitle = match title_full.split('\\').nth(1) {
203            Some(s) => Some(s.to_string()),
204            None => None,
205        };
206
207        let tile_properties = cnmb_types::TileProperties::from_lparse(cnmb, version, version.preview_tile_index, ignore_warnings)?;
208        let preview_loc = (tile_properties.frames[0].0 as u32, tile_properties.frames[0].1 as u32);
209        let difficulty_rating = DifficultyRating::from_difficulty_id(
210            cnmb.try_get_entry("BP_DMG")?
211                .try_get_i32()?[version.preview_tile_index] as u8
212        ).unwrap_or(DifficultyRating::Normal);
213
214        Ok(Self {
215            title,
216            subtitle,
217            preview_loc,
218            difficulty_rating,
219        })
220    }
221
222    /// Returns the full formated level title
223    pub fn get_full_title(&self) -> String {
224        let subtitle = "\\".to_string() + self.subtitle.as_ref().unwrap_or(&"".to_string()).as_str();
225        self.title.clone() + match self.subtitle {
226            Some(_) => subtitle.as_str(),
227            None => "",
228        }
229    }
230
231    fn get_tile_property(&self) -> cnmb_types::TileProperties {
232        cnmb_types::TileProperties {
233            solid: false,
234            transparency: consts::CLEAR,
235            damage_type: cnmb_types::DamageType::Lava(self.difficulty_rating.get_difficulty_id() as i32),
236            anim_speed: Duration(1),
237            frames: vec![(self.preview_loc.0 as i32, self.preview_loc.1 as i32)],
238            collision_data: cnmb_types::CollisionType::Box(crate::Rect { x: 0, y: 0, w: 0, h: 0 }),
239        }
240    }
241}
242
243/// The overarching level data structure. Holds everything pertaining to a level in CNM Online.
244#[cfg_attr(any(feature = "level_data", doc), derive(serde::Serialize, serde::Deserialize))]
245#[derive(Debug)]
246pub struct LevelData {
247    /// Version specifications
248    pub version: VersionSpecs,
249    /// A list of all the objects in the level
250    pub spawners: Vec<cnms_types::Spawner>,
251    /// A grid of cells (tiles, blocks, whatever you want to call them) in the level.
252    pub cells: cnmb_types::Cells,
253    /// An array of tile properties. A tile ID corresponds to a entry in this array.
254    pub tile_properties: Vec<cnmb_types::TileProperties>,
255    /// Level select metadata
256    pub metadata: LevelMetaData,
257    /// An array of background layers. Ones futher in the array draw over ones in
258    /// front (smaller indices).
259    pub background_layers: Vec<BackgroundLayer>,
260}
261
262impl LevelData {
263    /// Create a blank level from a level version.
264    /// 
265    /// Only version supported is version ID 1.
266    pub fn from_version(version: u32) -> Result<Self, Error> {
267        let version = VersionSpecs::from_version(version)?;
268        let background_layers = (0..version.background_layers).map(|_| cnmb_types::BackgroundLayer::default()).collect();
269
270        Ok(Self {
271            version,
272            spawners: Vec::new(),
273            tile_properties: Vec::new(),
274            cells: cnmb_types::Cells::new(512, 256),
275            metadata: LevelMetaData {
276                title: "Untitled".to_string(),
277                subtitle: None,
278                preview_loc: (0, 0),
279                difficulty_rating: DifficultyRating::Normal,
280            },
281            background_layers,
282        })
283    }
284
285    /// Load a level from the .cnmb and .cnms lparse files
286    /// 
287    /// Ignore warnings loads levels with illogical configurations of elements, like for example
288    /// a cell with a ID that goes beyond the tile properties array, but sometimes this has to be used
289    /// because old CNM levels sometimes have garbage data that will trigger the warnings anyway. If
290    /// a warning does get triggered anyway, it will return it as an error.
291    /// 
292    /// Hasn't been tested but levels created with the API under normal circumstances shouldn't
293    /// trigger any warnings.
294    pub fn from_lparse(cnmb: &LParse, cnms: &LParse, ignore_warnings: bool) -> Result<Self, Error> {
295        if cnmb.version.version != cnms.version.version {
296            return Err(Error::MismatchedVersions(cnmb.version.version, cnms.version.version));
297        }
298
299        let version = VersionSpecs::from_version(cnmb.version.version)?;
300        let tile_properties = Self::tile_properties_from_lparse(cnmb, &version, ignore_warnings)?;
301        let cells = cnmb_types::Cells::from_lparse(cnmb, tile_properties.len())?;
302        let spawners = Self::spawners_from_lparse(cnms, &version, ignore_warnings)?;
303        let metadata = LevelMetaData::from_lparse(cnmb, cnms, &version, ignore_warnings)?;
304        let background_layers = Self::background_layers_from_lparse(cnmb, &version)?;
305
306        Ok(Self {
307            version,
308            tile_properties,
309            cells,
310            spawners,
311            metadata,
312            background_layers,
313        })
314    }
315
316    /// Saves to a the 2 files. Creates them if they're not there, or overwrites them if they are.
317    pub fn save(&self, cnmb: &mut LParse, cnms: &mut LParse) {
318        cnms_types::save_spawner_vec(cnms, &self.version, self.metadata.get_full_title(), &self.spawners);
319        cnmb_types::save_background_vec(cnmb, &self.version, &self.background_layers);
320        cnmb_types::save_tile_properties_vec(cnmb, &self.version, &self.metadata.get_tile_property(), &self.tile_properties);
321        self.cells.save(cnmb, self.tile_properties.len() + 1, &self.version);
322    }
323
324    fn tile_properties_from_lparse(cnmb: &LParse, version: &VersionSpecs, ignore_warnings: bool) -> Result<Vec<cnmb_types::TileProperties>, Error> {
325        let mut tile_properties = Vec::new();
326
327        for index in 0..cnmb.try_get_entry("BLOCKS_HEADER")?.try_get_i32()?[2] as usize {
328            if index == version.preview_tile_index {
329                continue;
330            }
331            let tile = cnmb_types::TileProperties::from_lparse(cnmb, version, index, ignore_warnings)?;
332            match tile {
333                TileProperties {
334                    damage_type: cnmb_types::DamageType::None,
335                    anim_speed: Duration(1),
336                    frames,
337                    collision_data: cnmb_types::CollisionType::Box(crate::Rect { x: 0, y: 0, w: 32, h: 32 }),
338                    ..
339                } if frames.len() == 1 && frames[0] == (0, 0) => {},
340                tile => tile_properties.push(tile),
341            };
342        }
343
344        Ok(tile_properties)
345    }
346
347    fn spawners_from_lparse(cnms: &LParse, version: &VersionSpecs, ignore_warnings: bool) -> Result<Vec<cnms_types::Spawner>, Error> {
348        let mut spawners = Vec::new();
349
350        for index in 0..cnms.try_get_entry("NUM_SPAWNERS")?.try_get_i32()?[0] as usize {
351            spawners.push(cnms_types::Spawner::from_lparse(cnms, version, index, ignore_warnings)?);
352        }
353
354        Ok(spawners)
355    }
356
357    fn background_layers_from_lparse(cnmb: &LParse, version: &VersionSpecs) -> Result<Vec<BackgroundLayer>, Error> {
358        let mut background_layers = Vec::new();
359
360        for index in 0..version.background_layers {
361            background_layers.push(cnmb_types::BackgroundLayer::from_lparse(cnmb, version, index)?);
362        }
363
364        Ok(background_layers)
365    }
366}