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 = match node.attribute("type") {
Some(ty) => ty,
None => "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)
}
}