pyxel/
tmx_parser.rs

1use std::fs::File;
2use std::io::Read;
3
4use serde::Deserialize;
5
6use crate::settings::TILE_SIZE;
7use crate::tilemap::{ImageSource, Tilemap};
8use crate::utils::remove_whitespace;
9use crate::SharedTilemap;
10
11#[derive(Debug, Deserialize)]
12struct Tileset {
13    firstgid: u32,
14    columns: Option<u32>,
15}
16
17#[derive(Debug, Deserialize)]
18struct LayerData {
19    #[serde(rename = "encoding")]
20    encoding: String,
21    #[serde(rename = "$value")]
22    tiles: String,
23}
24
25#[derive(Debug, Deserialize)]
26struct Layer {
27    width: u32,
28    height: u32,
29    data: LayerData,
30}
31
32#[derive(Debug, Deserialize)]
33struct TiledMapFile {
34    tilewidth: u32,
35    tileheight: u32,
36    #[serde(rename = "tileset", default)]
37    tilesets: Vec<Tileset>,
38    #[serde(rename = "layer", default)]
39    layers: Vec<Layer>,
40}
41
42impl Tilemap {
43    pub fn from_tmx(filename: &str, layer_index: u32) -> SharedTilemap {
44        macro_rules! assert_or_break {
45            ($condition:expr, $fmt:expr $(,$arg:tt)*) => {
46                if !$condition {
47                    println!($fmt, $($arg)*);
48                    break;
49                }
50            };
51        }
52
53        #[allow(clippy::never_loop)]
54        loop {
55            let file = File::open(filename);
56            assert_or_break!(file.is_ok(), "Failed to open file '{filename}'");
57            let mut file = file.unwrap();
58
59            let mut tmx_text = String::new();
60            assert_or_break!(
61                file.read_to_string(&mut tmx_text).is_ok(),
62                "Failed to read TMX file"
63            );
64
65            let tmx = serde_xml_rs::from_str(&tmx_text);
66            assert_or_break!(tmx.is_ok(), "Failed to parse TMX file");
67            let tmx: TiledMapFile = tmx.unwrap();
68
69            assert_or_break!(
70                tmx.tilewidth == TILE_SIZE && tmx.tileheight == TILE_SIZE,
71                "TMX file's tile size is not {TILE_SIZE}x{TILE_SIZE}"
72            );
73
74            assert_or_break!(!tmx.tilesets.is_empty(), "Tileset not found in TMX file");
75            let tileset = &tmx.tilesets[0];
76            assert_or_break!(
77                tileset.columns.is_some(),
78                "Tileset is not embedded in TMX file"
79            );
80            let tileset_columns = tileset.columns.unwrap();
81
82            assert_or_break!(
83                layer_index < tmx.layers.len() as u32,
84                "Layer {layer_index} not found in TMX file"
85            );
86            let layer = &tmx.layers[layer_index as usize];
87            assert_or_break!(
88                layer.data.encoding == "csv",
89                "TMX file's encoding is not CSV"
90            );
91
92            let layer_data: Vec<u32> = remove_whitespace(&layer.data.tiles)
93                .split(',')
94                .map(|s| s.parse::<u32>().unwrap())
95                .collect();
96
97            let tilemap = Self::new(layer.width, layer.height, ImageSource::Index(0));
98            {
99                let mut tilemap = tilemap.lock();
100                for (i, tile_id) in layer_data.iter().enumerate() {
101                    let x = i % layer.width as usize;
102                    let y = i / layer.width as usize;
103                    let tile_id = if *tile_id > tileset.firstgid {
104                        tile_id - tileset.firstgid
105                    } else {
106                        0
107                    };
108                    let tile_x = (tile_id % tileset_columns) as u8;
109                    let tile_y = (tile_id / tileset_columns) as u8;
110                    tilemap.canvas.write_data(x, y, (tile_x, tile_y));
111                }
112            }
113
114            return tilemap;
115        }
116
117        // Return a blank tilemap due to an error
118        Self::new(1, 1, ImageSource::Index(0))
119    }
120}