tiled-json 0.1.4

Tiled parser for the JSON format
use std::str;
use std::path::PathBuf;
use std::ffi::OsStr;
use std::fs::File;
use std::collections::HashMap;

use {GlobalTile, LocalTile};

use serde::{Deserialize, Deserializer};

use serde_json;
use serde_json::Value as JsonValue;
use serde_json::Error as JsonError;

/// Tiled Tileset, containing everything we need to render tiles from
/// this set as well as decide how to do collision checks
#[derive(Clone, Debug, Deserialize)]
pub struct Tileset {
    /// Name of the tileset specified by its creator
    pub name: String,
    /// Global ID of the first tile which is part of this set. Global IDs
    /// are meaningless unless applied to a list of Tilesets associated with
    /// the correct map.
    pub firstgid: GlobalTile,
    
    /// Number of tiles contained in this map
    pub tilecount: u32,
    /// Height in pixels of each tile
    pub tileheight: u32,
    /// Width in pixels of each tile
    pub tilewidth: u32,
    
    /// The number of tiles per row in the image
    pub columns: u32,
    /// Path to the image representing this tileset
    /// TODO: Support multi-image sets?
    pub image: PathBuf,
    /// Expected height in pixels of the image
    pub imageheight: u32,
    /// Expected width in pixels of the image
    pub imagewidth: u32,
    /// Margin in the image between the edges and where the first tile starts
    pub margin: u32,
    /// Number of pixels between each tile
    pub spacing: u32,
    
    /// Key-Value pair properties specified for this tileset (game-specific data)
    pub properties: Option<HashMap<String, String>>,
    /// List of all the terrain types defined in this tileset. The values inside
    /// the `tiles` member correspond to indices in this array
    pub terrains: Vec<Terrain>,
    /// Key-Value pair properties associated with specific tiles in this set
    pub tileproperties: TileProperties,
    /// List of tiles that are associated with specific terrain, and which
    /// corners belong to which terrain type.
    pub tiles: TileTerrain,
}

impl Tileset {
    /// Given a JsonValue for a tileset, and the path of the level it is a member of,
    /// try to parse the tileset or load and parse it from an external file.
    pub fn load<P: AsRef<OsStr>>(data: JsonValue, data_path: &P) -> Result<Tileset, JsonError> {
        use serde::de::Error;
        // The data we're deserializing here must be a Json table
        let mut data = match data {
            JsonValue::Object(data) => data,
            _ => return Err(JsonError::custom("Tileset data was not an Object")),
        };
        
        // If data contains a "source" field, we're dealing with an
        // external tileset, and we must load that file.
        Ok(match data.remove("source") {
            Some(JsonValue::String(source)) => {
                // firstgid is not stored in the external data, so we
                // must save it from here for later
                let firstgid = match data.remove("firstgid").and_then(|i| i.as_u64()) {
                    Some(i) => i as u32,
                    None => return Err(JsonError::custom("Tileset had no firstgid")),
                };
                
                // Start with the path to the level
                let mut path = PathBuf::from(data_path);
                path.pop(); // Path is now the level directory
                path.push(source); // Path is the tileset to load
                
                // Try to open the file! We can just use the try!() macro
                // because serde_json::Error has a From converion from io::Error
                let mut file = try!(File::open(&path));
                
                // Parse the tileset file into an ExternalTileset structure
                let ext: ExternalTileset = try!(serde_json::from_reader(&mut file));
                
                path.pop();
                path.push(&ext.image);
                
                Tileset {
                    name: ext.name,
                    firstgid: GlobalTile(firstgid),
                    
                    tilecount: ext.tilecount,
                    tileheight: ext.tileheight,
                    tilewidth: ext.tilewidth,
                    
                    columns: ext.columns,
                    image: path,
                    imageheight: ext.imageheight,
                    imagewidth: ext.imagewidth,
                    margin: ext.margin,
                    spacing: ext.spacing,
                    
                    properties: ext.properties,
                    terrains: ext.terrains,
                    tileproperties: ext.tileproperties,
                    tiles: ext.tiles,
                }
            },
            // The tileset is inlined in the level, just parse its data
            _ => {
                let mut tileset: Tileset = try!(serde_json::from_value(JsonValue::Object(data)));
                let mut path = PathBuf::from(data_path);
                path.pop();
                path.push(&tileset.image);
                tileset.image = path;
                tileset
            }
        })
    }
    
    pub fn contains_tile(&self, id: GlobalTile) -> bool {
        if id.0 < self.firstgid.0 { return false; }
        let local = id.0 - self.firstgid.0;
        local < self.tilecount
    }
}

#[derive(Clone, Debug, Deserialize)]
struct ExternalTileset {
    name: String,
    
    tilecount: u32,
    tileheight: u32,
    tilewidth: u32,
    
    columns: u32,
    image: PathBuf,
    imageheight: u32,
    imagewidth: u32,
    margin: u32,
    spacing: u32,
    
    properties: Option<HashMap<String, String>>,
    terrains: Vec<Terrain>,
    tileproperties: TileProperties,
    tiles: TileTerrain,
}

#[derive(Clone, Debug)]
pub struct TileProperties {
    pub tiles: HashMap<LocalTile, HashMap<String, String>>,
}

impl Deserialize for TileProperties {
    fn deserialize<D: Deserializer>(d: &mut D) -> Result<Self, D::Error> {
        // Tiled uses string keys because it's a sparse array,
        // so we're just going to parse it like that and then
        // convert them to LocalTiles
        let data: HashMap<String, HashMap<String, String>>;
        data = try!(Deserialize::deserialize(d));
        
        let mut props = HashMap::new();
        for (k, v) in data {
            // Allows us to return an error when a bad key is present
            use serde::de::Error;
            
            // We'll return an error if the key isn't a valid integer
            let id: u32 = match str::parse(&k) {
                Ok(id) => id,
                Err(_) => return Err(D::Error::custom("tileproperties contained a non-integer key"))
            };
            
            props.insert(LocalTile(id), v);
        }
        
        Ok(TileProperties {
            tiles: props,
        })
    }
}

#[derive(Clone, Debug)]
pub struct TileTerrain {
    pub tiles: HashMap<LocalTile, [u32; 4]>
}

impl Deserialize for TileTerrain {
    fn deserialize<D: Deserializer>(d: &mut D) -> Result<Self, D::Error> {
        #[derive(Deserialize)]
        struct Data {
            terrain: [u32; 4]
        }
        
        // Tiled uses string keys because it's a sparse array,
        // so we're just going to parse it like that and then
        // convert them to LocalTiles
        let data: HashMap<String, Data>;
        data = try!(Deserialize::deserialize(d));
        
        let mut terrains = HashMap::new();
        for (k, v) in data {
            // Allows us to return an error when a bad key is present
            use serde::de::Error;
            
            // We'll return an error if the key isn't a valid integer
            let id: u32 = match str::parse(&k) {
                Ok(id) => id,
                Err(_) => return Err(D::Error::custom("tileproperties contained a non-integer key"))
            };
            
            terrains.insert(LocalTile(id), v.terrain);
        }
        
        Ok(TileTerrain {
            tiles: terrains,
        })
    }
}

#[derive(Clone, Debug, Deserialize)]
pub struct Terrain {
    pub name: String,
    pub tile: LocalTile,
}

/// Test to ensure we can deserialize an ExternalTileset
#[test]
fn deserialize_external() {
    use serde_json::from_str;
    
    let data = include_str!("../test-assets/tilesets/goodly-2x.json");
    let _: ExternalTileset = from_str(data).unwrap();
}