twmap/map/edit/
mod.rs

1use crate::constants;
2use crate::convert::{To, TryTo};
3use crate::map::*;
4
5use fixed::types::{I17F15, I27F5};
6use image::{ImageError, Pixel, RgbaImage};
7use vek::num_traits::CheckedAdd;
8use vek::Extent2;
9
10use std::fs;
11use std::io;
12use std::io::{BufReader, Read, Seek};
13use std::path::Path;
14
15mod dilate;
16mod extend;
17mod mirror;
18mod optimize_mapres;
19mod rotate;
20mod scale;
21mod shrink;
22mod tiles;
23mod unused;
24
25pub use dilate::dilate;
26pub use extend::edge_extend_ndarray;
27pub use shrink::shrink_ndarray;
28pub use tiles::{
29    DDNetFixPhysicsRotation, DDNetMigrateSpeedup, DDNetNormalizePhysicsRotation, EditTile,
30    TileFlips, ZeroAir, ZeroUnusedParts,
31};
32
33/// Desired amount of sub-tiles (tiles / 32) in default camera view
34const AMOUNT: u32 = 1150 * 1000;
35/// Maximum amount of sub-tiles horizontally
36const MAX_WIDTH: i32 = 1500;
37/// Maximum amount of sub-tiles vertically
38const MAX_HEIGHT: i32 = 1150;
39
40/// The maximum amount of tiles horizontally and vertically that the camera can cover.
41/// Note that it can never be the maximum in both directions.
42/// Default zoom level is assumed.
43pub const MAX_CAMERA_DIMENSIONS: Extent2<I27F5> = Extent2 {
44    w: I27F5::from_bits(MAX_WIDTH),
45    h: I27F5::from_bits(MAX_HEIGHT),
46};
47
48/// The aspect ratio is width / height.
49/// Returns the dimension in tiles that Teeworlds/DDNet would render in that aspect ratio.
50/// Default zoom level is assumed.
51pub fn camera_dimensions(aspect_ratio: f32) -> Extent2<f32> {
52    /*
53    width (x), height (y) calculation from the aspect ratio
54        x * y = x * yß
55    <=>     x = (x * y) / y
56    <=>   x^2 = (x * y) * (x / y)
57    <=>   x^2 = AMOUNT * aspect_ratio
58    <=>     x = sqrt(AMOUNT * aspect_ratio)
59    */
60    let mut width = (AMOUNT as f32 * aspect_ratio).sqrt();
61    // x * y = x * y <=> y = x * (y / x) <=> y = x / aspect_ratio
62    let mut height = width / aspect_ratio;
63    // If a calculated length exceeds the maximum, cap it at the respective maximum
64    if width > MAX_WIDTH as f32 {
65        width = MAX_WIDTH as f32;
66        height = width / aspect_ratio;
67    }
68    if height > MAX_HEIGHT as f32 {
69        height = MAX_HEIGHT as f32;
70        width = height * aspect_ratio;
71    }
72    Extent2::new(width / 32., height / 32.)
73}
74
75impl TwMap {
76    /// Returns a reference to the specified physics layer, if the map contains it.
77    /// Note that every map must have a Game layer to pass the checks.
78    pub fn find_physics_layer<T: PhysicsLayer>(&self) -> Option<&T> {
79        match self
80            .physics_group()
81            .layers
82            .iter()
83            .rev()
84            .find(|l| l.kind() == T::kind())
85        {
86            None => None,
87            Some(l) => T::get(l),
88        }
89    }
90
91    /// Returns a mutable reference to the specified physics layer, if the map contains it.
92    /// Note that every map must have a Game layer to pass the checks.
93    pub fn find_physics_layer_mut<T: PhysicsLayer>(&mut self) -> Option<&mut T> {
94        match self
95            .physics_group_mut()
96            .layers
97            .iter_mut()
98            .rev()
99            .find(|l| l.kind() == T::kind())
100        {
101            None => None,
102            Some(l) => T::get_mut(l),
103        }
104    }
105}
106
107impl LayerKind {
108    // returns index suitable for checking for duplicates
109    const fn duplicate_index(&self) -> usize {
110        match self {
111            LayerKind::Game => 0,
112            LayerKind::Front => 1,
113            LayerKind::Switch => 2,
114            LayerKind::Tele => 3,
115            LayerKind::Speedup => 4,
116            LayerKind::Tune => 5,
117            _ => panic!(), // may exist multiple times
118        }
119    }
120}
121
122impl TwMap {
123    pub fn parse_path_unchecked<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
124        let path = path.as_ref();
125
126        let metadata = fs::metadata(path)?;
127        if metadata.is_file() {
128            let map_data = fs::read(path)?;
129            TwMap::parse(&map_data)
130        } else if metadata.is_dir() {
131            TwMap::parse_dir_unchecked(path)
132        } else {
133            Err(io::Error::new(io::ErrorKind::InvalidData, "Neither a file nor directory").into())
134        }
135    }
136
137    /// Parses binary as well as MapDir maps.
138    pub fn parse_path<P: AsRef<Path>>(path: P) -> Result<Self, Error> {
139        let map = TwMap::parse_path_unchecked(&path)?;
140        map.check()?;
141        Ok(map)
142    }
143}
144
145impl Sound {
146    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Sound, opus_headers::ParseError> {
147        let path = path.as_ref();
148        let name = path.file_stem().unwrap().to_str().ok_or_else(|| {
149            io::Error::new(
150                io::ErrorKind::InvalidInput,
151                "The file name includes invalid utf-8",
152            )
153        })?;
154        let reader = std::fs::File::open(path)?;
155        Self::from_reader(name, reader)
156    }
157
158    /// Creates a sound from a reader.
159    pub fn from_reader<R: Read>(
160        name: &str,
161        mut reader: R,
162    ) -> Result<Sound, opus_headers::ParseError> {
163        let mut buf = Vec::new();
164        reader.read_to_end(&mut buf)?;
165        opus_headers::parse_from_read(&buf[..])?;
166
167        Ok(Sound {
168            name: name.to_string(),
169            data: buf.into(),
170        })
171    }
172}
173
174impl EmbeddedImage {
175    /// Creates a embedded image using a provided image file.
176    /// This function supports png and any other file formats you activate via features of the crate `image`.
177    /// In your `Cargo.toml` you should turn off the default features of `image` by setting `default-features = false`.
178    /// Then you can activate different file formats by activating specific features.
179    /// Errors with an io::ErrorKind::InvalidInput if the filename includes invalid utf-8
180    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<EmbeddedImage, ImageError> {
181        let path = path.as_ref();
182        let name = match path.file_stem().unwrap().to_str() {
183            Some(str) => str,
184            None => {
185                return Err(io::Error::new(
186                    io::ErrorKind::InvalidInput,
187                    "The file name includes invalid utf-8",
188                )
189                .into())
190            }
191        };
192        let reader = std::fs::File::open(path)?;
193        Self::from_reader(name, reader)
194    }
195
196    /// Creates an embedded image from the provided reader.
197    /// This function wraps the provided reader around a [BufReader] so you don't have to do it.
198    pub fn from_reader<R: Read + Seek>(name: &str, inner: R) -> Result<EmbeddedImage, ImageError> {
199        let reader =
200            image::ImageReader::with_format(BufReader::new(inner), image::ImageFormat::Png);
201        let image = reader.decode()?.into_rgba8().into();
202        Ok(EmbeddedImage {
203            name: name.to_string(),
204            image,
205        })
206    }
207}
208
209impl ExternalImage {
210    /// Tries to embed the external images by loading them from the file `<mapres_directory>/<image_name>`
211    pub fn embed<P: AsRef<Path>>(&self, mapres_directory: P) -> Result<EmbeddedImage, ImageError> {
212        let mut file_name = self.name.clone();
213        file_name.push_str(".png");
214
215        let path = mapres_directory.as_ref().join(&file_name);
216
217        EmbeddedImage::from_file(path)
218    }
219}
220
221impl TwMap {
222    /// Tries to embed the external images by loading them from the file `<mapres_directory>/<image_name>`
223    pub fn embed_images<P: AsRef<Path>>(&mut self, mapres_directory: P) -> Result<(), ImageError> {
224        let mut result = Ok(());
225        for image in &mut self.images {
226            if let Image::External(ex) = image {
227                match ex.embed(mapres_directory.as_ref()) {
228                    Ok(emb) => *image = Image::Embedded(emb),
229                    Err(err) => {
230                        if result.is_ok() {
231                            result = Err(err);
232                        }
233                    }
234                }
235            }
236        }
237        result
238    }
239
240    /// Embed images with the twstorage file paths.
241    /// This will take the config directory into account.
242    pub fn embed_images_auto(&mut self) -> Result<(), ImageError> {
243        let mut result = Ok(());
244
245        fn read_mapres(name: &str, version: Version) -> Result<EmbeddedImage, ImageError> {
246            let path = format!("mapres/{name}.png");
247            let file = twstorage::read_file(&path, version.into())?;
248            EmbeddedImage::from_reader(name, file)
249        }
250
251        for image in &mut self.images {
252            if let Image::External(ext) = image {
253                match read_mapres(&ext.name, self.version) {
254                    Ok(emb) => *image = Image::Embedded(emb),
255                    Err(err) => {
256                        if result.is_ok() {
257                            result = Err(err);
258                        }
259                    }
260                }
261            }
262        }
263
264        result
265    }
266}
267
268impl TwMap {
269    /// For easy remapping of all image indices in tiles layers and quads.
270    pub fn edit_image_indices(&mut self, edit_fn: impl Fn(Option<u16>) -> Option<u16>) {
271        for group in &mut self.groups {
272            for layer in &mut group.layers {
273                match layer {
274                    Layer::Tiles(l) => l.image = edit_fn(l.image),
275                    Layer::Quads(l) => l.image = edit_fn(l.image),
276                    _ => {}
277                }
278            }
279        }
280    }
281
282    /// For easy remapping of all envelope indices in tiles, quads and sounds layers.
283    pub fn edit_env_indices(&mut self, edit_fn: impl Fn(Option<u16>) -> Option<u16>) {
284        for group in &mut self.groups {
285            for layer in &mut group.layers {
286                use Layer::*;
287                match layer {
288                    Tiles(l) => l.color_env = edit_fn(l.color_env),
289                    Quads(l) => {
290                        for quad in &mut l.quads {
291                            quad.color_env = edit_fn(quad.color_env);
292                            quad.position_env = edit_fn(quad.position_env);
293                        }
294                    }
295                    Sounds(l) => {
296                        for source in &mut l.sources {
297                            source.position_env = edit_fn(source.position_env);
298                            source.sound_env = edit_fn(source.sound_env);
299                        }
300                    }
301                    _ => {}
302                }
303            }
304        }
305    }
306
307    /// For easy remapping of all sound indices in sounds layers.
308    pub fn edit_sound_indices(&mut self, edit_fn: impl Fn(Option<u16>) -> Option<u16>) {
309        for group in &mut self.groups {
310            for layer in &mut group.layers {
311                if let Layer::Sounds(l) = layer {
312                    l.sound = edit_fn(l.sound)
313                }
314            }
315        }
316    }
317}
318
319/// Returns the table for the OPAQUE tile flag, derived from the image data
320pub fn calc_opaque_table(image: &RgbaImage) -> [[bool; 16]; 16] {
321    let tile_width = image.width() / 16;
322    let tile_height = image.height() / 16;
323    let mut table = [[false; 16]; 16];
324    if tile_width != tile_height {
325        return table;
326    }
327
328    for tile_y in 0..16 {
329        let tile_y_pos = tile_y * tile_width;
330        for tile_x in 0..16 {
331            let tile_x_pos = tile_x * tile_width;
332            let mut opaque = true;
333
334            'outer: for pixel_y in 0..tile_width {
335                for pixel_x in 0..tile_width {
336                    let y = tile_y_pos + pixel_y;
337                    let x = tile_x_pos + pixel_x;
338                    if image.get_pixel(x, y).channels()[3] < 250 {
339                        opaque = false;
340                        break 'outer;
341                    }
342                }
343            }
344
345            table[tile_y.try_to::<usize>()][tile_x.try_to::<usize>()] = opaque;
346        }
347    }
348    table[0][0] = false; // top left tile is manually emptied
349    table
350}
351
352impl Image {
353    /// Returns the table for the OPAQUE tile flag of the image.
354    pub fn calc_opaque_table(&self, version: Version) -> [[bool; 16]; 16] {
355        match self {
356            Image::External(image) => {
357                constants::external_opaque_table(&image.name, version).unwrap_or([[false; 16]; 16])
358            }
359            Image::Embedded(image) => calc_opaque_table(image.image.unwrap_ref()),
360        }
361    }
362}
363
364impl TwMap {
365    /// Fill in all OPAQUE tile flags.
366    pub fn process_tile_flag_opaque(&mut self) {
367        let tables: Vec<[[bool; 16]; 16]> = self
368            .images
369            .iter()
370            .map(|image| image.calc_opaque_table(self.version))
371            .collect();
372        for group in &mut self.groups {
373            for layer in &mut group.layers {
374                if let Layer::Tiles(layer) = layer {
375                    let opaque_table = {
376                        if layer.color.a != 255 {
377                            &[[false; 16]; 16]
378                        } else {
379                            match layer.image {
380                                None => &[[false; 16]; 16],
381                                Some(index) => &tables[index.to::<usize>()],
382                            }
383                        }
384                    };
385                    let tiles = layer.tiles.unwrap_mut();
386                    for tile in tiles {
387                        let opaque_value =
388                            opaque_table[tile.id.to::<usize>() / 16][tile.id.to::<usize>() % 16];
389                        tile.flags.set(TileFlags::OPAQUE, opaque_value);
390                    }
391                }
392            }
393        }
394    }
395
396    /// Set the width and height of external images to their default values.
397    pub fn set_external_image_dimensions(&mut self) {
398        for image in &mut self.images {
399            if let Image::External(ex) = image {
400                if let Some(size) = constants::external_dimensions(&ex.name, self.version) {
401                    ex.size = size;
402                }
403            }
404        }
405    }
406}
407
408impl QuadsLayer {
409    // TODO: somehow make atomic + the return value reasonable
410    fn shift(&mut self, offset: Vec2<I17F15>) -> Option<()> {
411        for quad in &mut self.quads {
412            quad.position = quad.position.checked_add(&offset)?;
413            for corner in &mut quad.corners {
414                *corner = corner.checked_add(&offset)?;
415            }
416        }
417        Some(())
418    }
419}
420
421impl SoundsLayer {
422    // TODO: somehow make the return value reasonable
423    fn shift(&mut self, offset: Vec2<I17F15>) -> Option<()> {
424        for source in &mut self.sources {
425            source
426                .area
427                .set_position(source.area.position().checked_add(&offset)?);
428        }
429        Some(())
430    }
431}
432
433impl TwMap {
434    /// Move cosmetic layers from the physics group into separate groups, keeping the render order
435    pub fn isolate_physics_layers(&mut self) {
436        let index = self
437            .groups
438            .iter()
439            .position(|g| g.is_physics_group())
440            .unwrap();
441        let game_group = &mut self.groups[index];
442        let mut front_group = Group {
443            name: String::from("v Front"),
444            ..Group::physics()
445        };
446        let mut back_group = Group {
447            name: String::from("^ Back"),
448            ..Group::physics()
449        };
450        let mut i = 0;
451        let mut after = false;
452        while i < game_group.layers.len() {
453            if !game_group.layers[i].kind().is_physics_layer() {
454                if after {
455                    back_group.layers.push(game_group.layers.remove(i));
456                } else {
457                    front_group.layers.push(game_group.layers.remove(i));
458                }
459            } else {
460                if game_group.layers[i].kind() == LayerKind::Game {
461                    after = true;
462                }
463                i += 1;
464            }
465        }
466        if !back_group.layers.is_empty() {
467            self.groups.insert(index + 1, back_group);
468        }
469        if !front_group.layers.is_empty() {
470            self.groups.insert(index, front_group);
471        }
472    }
473}
474
475fn calc_new_offset(
476    former_offset: I27F5,
477    origin_shift: I27F5,
478    parallax: i32,
479    align_size: I27F5,
480) -> Option<I27F5> {
481    let origin_shift_parallaxed = origin_shift
482        .checked_mul_int(parallax)?
483        .checked_div_int(100)?;
484    let offset_offset = origin_shift_parallaxed.checked_sub(align_size)?;
485    former_offset
486        .checked_add(offset_offset)?
487        .checked_mul_int(-1)
488}
489
490fn calc_new_clip_pos(align_size: I27F5, clip_pos: I27F5, clip_size: I27F5) -> Option<I27F5> {
491    let new_clip_corner_pos = clip_pos.checked_add(clip_size)?;
492    align_size.checked_sub(new_clip_corner_pos)
493}