1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
//! Loader for maps authored with [Tiled](https://www.mapeditor.org/), to
//! translate them into [World]s.

use std::collections::HashMap;

use anyhow::Context;
use common::{Chunk, Point, TileIndex, World, CHUNKS_X, CHUNKS_Y, CHUNK_HEIGHT, CHUNK_WIDTH};
use roxmltree::Document;

/// The three different layers used in the maps.
#[derive(Clone, Copy)]
enum Layer {
    Spawns,
    Floor,
    Wall,
}

/// Creates a [World] based on the Tiled .tmx map in `tmx_file`. The tmx file
/// must use csv encoding for the layers.
///
/// Note: `tmx_file` is not a path, it should contain the raw XML contained
/// within the .tmx file, e.g. from [include_str].
pub fn load_map(tmx_file: &str) -> anyhow::Result<World> {
    let mut chunks = vec![
        Some(Chunk {
            walls: vec![None; CHUNK_WIDTH * CHUNK_HEIGHT],
            floors: vec![None; CHUNK_WIDTH * CHUNK_HEIGHT],
            spawns: Vec::new(),
            pcs: HashMap::new(),
        });
        CHUNKS_X * CHUNKS_Y
    ];
    let mut set_tile = |x: usize, y: usize, tile_index: u32, layer: Layer| {
        let chunk_x = x / CHUNK_WIDTH;
        let chunk_y = y / CHUNK_HEIGHT;
        let tile_x = x % CHUNK_WIDTH;
        let tile_y = y % CHUNK_HEIGHT;
        let tile_i = tile_x + tile_y * CHUNK_WIDTH;
        let chunk = chunks[chunk_x + chunk_y * CHUNKS_X].as_mut().unwrap();
        match layer {
            Layer::Spawns => chunk.spawns.push(Point(tile_x as i32, tile_y as i32)),
            Layer::Floor => chunk.floors[tile_i] = Some(TileIndex(tile_index)),
            Layer::Wall => chunk.walls[tile_i] = Some(TileIndex(tile_index)),
        }
    };

    let tmx =
        Document::parse(tmx_file).context("Could not parse the .tmx file as an XML document")?;
    for tmx_descendant in tmx.descendants() {
        if !tmx_descendant.has_tag_name("layer") {
            continue;
        }
        let layer = match tmx_descendant.attribute("name") {
            Some("Spawns") => Layer::Spawns,
            Some("Floors") => Layer::Floor,
            Some("Walls") => Layer::Wall,
            _ => continue,
        };
        let csv_text = match tmx_descendant
            .descendants()
            .find(|node| node.has_tag_name("data") && node.attribute("encoding") == Some("csv"))
            .and_then(|node| node.text())
        {
            Some(node_text) => node_text,
            None => continue,
        };
        for (y, line) in csv_text.lines().skip_while(|s| s.is_empty()).enumerate() {
            if y >= CHUNKS_Y * CHUNK_HEIGHT {
                break;
            }
            for (x, tile_number) in line.split(',').enumerate() {
                if x >= CHUNKS_X * CHUNK_WIDTH {
                    break;
                }
                let tile_number = match tile_number.parse::<u32>() {
                    Ok(i) => i,
                    Err(err) => {
                        log::warn!("Map csv contains non-number tile: {err}");
                        continue;
                    }
                };
                if tile_number > 0 {
                    set_tile(x, y, tile_number - 1, layer);
                }
            }
        }
    }

    Ok(World { time: 0, chunks })
}