summaryrefslogblamecommitdiff
path: root/src/lowlevel/property.rs
blob: bf6d0c8fd7e4ef8c6d2934947ac85d7510f237ae (plain) (tree)
1
                                                            



















                                                                                                                                                                                                                 
                   

                                                                        

                                           
 
                                                            
                                                                       














                                                                                                     
                                                       
























































                                                                    
                       



















                                                                                

                           



                                  





                                         
                 







                                                                          



            
                    



                                            






















































































                                                                                   
                                                                               








                                             











                                                                                  



           






                                                                       


             











                                                        






                                              

           









































































                                                                                         


             















                                                              
 
use crate::{ensure_element, ensure_tag_name, get_attribute};
use anyhow::anyhow;
use rgb::RGBA8;

// https://doc.mapeditor.org/en/stable/reference/tmx-map-format/#property

// name: The name of the property.
// type: The type of the property. Can be string (default), int, float, bool, color, file or object (since 0.16, with color and file added in 0.17, and object added in 1.4).
// value: The value of the property. (default string is “”, default number is 0, default boolean is “false”, default color is #00000000, default file is “.” (the current file’s parent directory))

#[derive(Debug, PartialEq)]
pub enum Property {
    String { name: String, value: String },
    Int { name: String, value: isize },
    Float { name: String, value: f64 },
    Bool { name: String, value: bool },
    Color { name: String, value: RGBA8 },
    File { name: String, value: String },
    Object { name: String, value: usize },
}

#[allow(dead_code)]
impl Property {
    pub fn from_xml(node: roxmltree::Node) -> anyhow::Result<Property> {
        ensure_element!(node);
        ensure_tag_name!(node, "property");

        let name = get_attribute!(node, "name")?.to_owned();
        let property_type = node.attribute("type").unwrap_or("string");

        // handle the case that 'string' value is stored in element content instead of value atribute
        if property_type == "string" {
            if let Some(child) = node.first_child() {
                if let Some(text) = child.text() {
                    return Ok(Property::String {
                        name,
                        value: text.to_owned(),
                    });
                } else {
                    return Err(anyhow!("property element content is not of NodeType::Text"));
                }
            }
        }

        let raw_value = get_attribute!(node, "value")?;

        match property_type {
            "string" => Ok(Property::String {
                name,
                value: raw_value.to_owned(),
            }),
            "int" => Ok(Property::Int {
                name,
                value: raw_value.parse()?,
            }),
            "float" => Ok(Property::Float {
                name,
                value: raw_value.parse()?,
            }),
            "bool" => {
                let value = match raw_value {
                    "true" => Ok(true),
                    "false" => Ok(false),
                    _ => Err(anyhow!(
                        "boolean property has unexpected value: {}",
                        raw_value
                    )),
                };
                Ok(Property::Bool {
                    name,
                    value: value?,
                })
            }
            "color" => {
                let mut chars = raw_value.chars();
                chars.next(); // remove the `#`
                if let Some(color) = read_color::rgba(&mut chars) {
                    Ok(Property::Color {
                        name,
                        value: RGBA8 {
                            r: color[0],
                            g: color[1],
                            b: color[2],
                            a: color[3],
                        },
                    })
                } else {
                    Err(anyhow!("could not parse color"))
                }
            }
            "file" => Ok(Property::File {
                name,
                value: raw_value.to_owned(),
            }),
            "object" => Ok(Property::Object {
                name,
                value: raw_value.parse()?,
            }),
            _ => Err(anyhow!("unknown type {}", property_type)),
        }
    }

    #[allow(dead_code)]
    pub fn value_to_string(self) -> String {
        match self {
            Property::String { value, .. } => value,
            Property::Int { value, .. } => format!("{}", value),
            Property::Float { value, .. } => format!("{}", value),
            Property::Bool { value, .. } => match value {
                true => "true",
                false => "false",
            }
            .to_owned(),
            Property::Color { value, .. } => {
                format!("#{:x}{:x}{:x}{:x}", value.r, value.g, value.b, value.a)
            }
            Property::File { value, .. } => value,
            Property::Object { value, .. } => format!("{}", value),
        }
    }
}

// Wraps any number of custom properties
#[allow(dead_code)]
#[derive(Debug, PartialEq)]
pub struct Properties {
    pub properties: Vec<Property>,
}

impl Default for Properties {
    fn default() -> Self {
        Properties { properties: vec![] }
    }
}

impl Properties {
    pub fn from_xml(node: roxmltree::Node) -> anyhow::Result<Properties> {
        let mut properties: Vec<Property> = Vec::new();
        for property_node in node.children() {
            if property_node.has_tag_name("property") {
                properties.push(Property::from_xml(property_node)?)
            }
        }
        Ok(Properties { properties })
    }
}

#[cfg(test)]
mod parse_property {
    use crate::lowlevel::property::Property;
    use rgb::RGBA8;

    #[test]
    fn string_inside_attribute() {
        crate::parse_property_test!(
            Property,
            r##"<property name="enemyText" type="string" value="hello world"/>"##,
            Property::String {
                name: "enemyText".to_owned(),
                value: "hello world".to_owned()
            }
        );
    }

    #[test]
    fn string_inside_element() {
        crate::parse_property_test!(
            Property,
            r##"<property name="enemyText" type="string">hello world</property>"##,
            Property::String {
                name: "enemyText".to_owned(),
                value: "hello world".to_owned()
            }
        );
    }

    #[test]
    fn int() {
        crate::parse_property_test!(
            Property,
            r##"<property name="enemyText" type="int" value="-8"/>"##,
            Property::Int {
                name: "enemyText".to_owned(),
                value: -8
            }
        );
        crate::parse_property_test!(
            Property,
            r##"<property name="enemyText" type="int" value="3453"/>"##,
            Property::Int {
                name: "enemyText".to_owned(),
                value: 3453
            }
        );
    }

    #[test]
    fn float() {
        crate::parse_property_test!(
            Property,
            r##"<property name="ExperienceBoost" type="float" value="-8.8"/>"##,
            Property::Float {
                name: "ExperienceBoost".to_owned(),
                value: -8.8
            }
        );
        crate::parse_property_test!(
            Property,
            r##"<property name="ExperienceBoost" type="float" value="0.005"/>"##,
            Property::Float {
                name: "ExperienceBoost".to_owned(),
                value: 0.005
            }
        );
    }

    #[test]
    fn bool() {
        crate::parse_property_test!(
            Property,
            r##"<property name="isNight" type="bool" value="true"/>"##,
            Property::Bool {
                name: "isNight".to_owned(),
                value: true
            }
        );
        crate::parse_property_test!(
            Property,
            r##"<property name="isNight" type="bool" value="false"/>"##,
            Property::Bool {
                name: "isNight".to_owned(),
                value: false
            }
        );
    }

    #[test]
    fn color() {
        crate::parse_property_test!(
            Property,
            r##"<property name="enemyTint" type="color" value="#ffa33636"/>"##,
            Property::Color {
                name: "enemyTint".to_owned(),
                value: RGBA8 {
                    r: 255,
                    g: 163,
                    b: 54,
                    a: 54
                }
            }
        )
    }

    #[test]
    fn file() {
        crate::parse_property_test!(
            Property,
            r##"<property name="music" type="file" value="../music/cave.ogg"/>"##,
            Property::File {
                name: "music".to_owned(),
                value: "../music/cave.ogg".to_owned()
            }
        );
    }

    #[test]
    fn object() {
        crate::parse_property_test!(
            Property,
            r##"<property name="music" type="object" value="2583"/>"##,
            Property::Object {
                name: "music".to_owned(),
                value: 2583
            }
        );
    }

    #[test]
    fn string_without_explicit_type() {
        crate::parse_property_test!(
            Property,
            r##"<property name="door" value="true"/>"##,
            Property::String {
                name: "door".to_owned(),
                value: "true".to_owned()
            }
        );
    }
}

#[cfg(test)]
mod parse_properties {
    use crate::lowlevel::property::Properties;
    use crate::lowlevel::property::Property;
    use rgb::RGBA8;

    #[test]
    fn one_property() {
        crate::parse_property_test!(
            Properties,
            r##"<properties>
                <property name="enemyTint" type="color" value="#ffa33636"/>
            </properties>"##,
            Properties {
                properties: vec![Property::Color {
                    name: "enemyTint".to_owned(),
                    value: RGBA8 {
                        r: 255,
                        g: 163,
                        b: 54,
                        a: 54
                    }
                }]
            }
        );
    }

    #[test]
    fn two_properties() {
        crate::parse_property_test!(
            Properties,
            r##"<properties>
                    <property name="enemyTint" type="color" value="#ffa33636"/>
                    <property name="enemyDropChanceModifier" type="float" value="0.005"/>
            </properties>"##,
            Properties {
                properties: vec![
                    Property::Color {
                        name: "enemyTint".to_owned(),
                        value: RGBA8 {
                            r: 255,
                            g: 163,
                            b: 54,
                            a: 54
                        }
                    },
                    Property::Float {
                        name: "enemyDropChanceModifier".to_owned(),
                        value: 0.005
                    }
                ]
            }
        );
    }

    #[test]
    fn two_properties_with_comments() {
        crate::parse_property_test!(
            Properties,
            r##"<properties>
                <!-- what color the enemy should have -->
                <property name="enemyTint" type="color" value="#ffa33636"/>
                <!-- a nother comment -->
                <property name="enemyDropChanceModifier" type="float" value="0.005"/>
            </properties>"##,
            Properties {
                properties: vec![
                    Property::Color {
                        name: "enemyTint".to_owned(),
                        value: RGBA8 {
                            r: 255,
                            g: 163,
                            b: 54,
                            a: 54
                        }
                    },
                    Property::Float {
                        name: "enemyDropChanceModifier".to_owned(),
                        value: 0.005
                    }
                ]
            }
        );
    }

    #[test]
    fn zero_properties() {
        crate::parse_property_test!(
            Properties,
            r##"<properties></properties>"##,
            Properties { properties: vec![] }
        );
    }

    #[test]
    fn default_value() {
        let empty_properties: Properties = Default::default();

        assert_eq!(empty_properties.properties.len(), 0)
    }
}