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 { 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, } impl Default for Properties { fn default() -> Self { Properties { properties: vec![] } } } impl Properties { pub fn from_xml(node: roxmltree::Node) -> anyhow::Result { let mut properties: Vec = 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::String { name: "enemyText".to_owned(), value: "hello world".to_owned() } ); } #[test] fn string_inside_element() { crate::parse_property_test!( Property, r##"hello world"##, Property::String { name: "enemyText".to_owned(), value: "hello world".to_owned() } ); } #[test] fn int() { crate::parse_property_test!( Property, r##""##, Property::Int { name: "enemyText".to_owned(), value: -8 } ); crate::parse_property_test!( Property, r##""##, Property::Int { name: "enemyText".to_owned(), value: 3453 } ); } #[test] fn float() { crate::parse_property_test!( Property, r##""##, Property::Float { name: "ExperienceBoost".to_owned(), value: -8.8 } ); crate::parse_property_test!( Property, r##""##, Property::Float { name: "ExperienceBoost".to_owned(), value: 0.005 } ); } #[test] fn bool() { crate::parse_property_test!( Property, r##""##, Property::Bool { name: "isNight".to_owned(), value: true } ); crate::parse_property_test!( Property, r##""##, Property::Bool { name: "isNight".to_owned(), value: false } ); } #[test] fn color() { crate::parse_property_test!( Property, r##""##, 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::File { name: "music".to_owned(), value: "../music/cave.ogg".to_owned() } ); } #[test] fn object() { crate::parse_property_test!( Property, r##""##, Property::Object { name: "music".to_owned(), value: 2583 } ); } #[test] fn string_without_explicit_type() { crate::parse_property_test!( Property, r##""##, 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 { 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 { 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 { 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: vec![] } ); } #[test] fn default_value() { let empty_properties: Properties = Default::default(); assert_eq!(empty_properties.properties.len(), 0) } }