diff --git a/rust/springylib/src/lib.rs b/rust/springylib/src/lib.rs index 4193e3e..4637171 100644 --- a/rust/springylib/src/lib.rs +++ b/rust/springylib/src/lib.rs @@ -1 +1,2 @@ pub mod archive; +pub mod media; diff --git a/rust/springylib/src/media/mod.rs b/rust/springylib/src/media/mod.rs new file mode 100644 index 0000000..6bae95d --- /dev/null +++ b/rust/springylib/src/media/mod.rs @@ -0,0 +1 @@ +pub mod ui; diff --git a/rust/springylib/src/media/ui/image.rs b/rust/springylib/src/media/ui/image.rs new file mode 100644 index 0000000..0fb4b4b --- /dev/null +++ b/rust/springylib/src/media/ui/image.rs @@ -0,0 +1,30 @@ +use crate::media::ui::vec::deserialize_vec2; +use crate::media::ui::FadeMode; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct UiImage { + pub texture: String, + #[serde(deserialize_with = "deserialize_vec2")] + pub position: [i32; 2], + #[serde(deserialize_with = "deserialize_vec2")] + pub size: [i32; 2], + #[serde(rename = "fademode", default)] + pub fade_mode: FadeMode, +} + +#[cfg(test)] +mod tests { + use crate::media::ui::image::UiImage; + + // language=xml + const IMAGE: &str = ""; + + #[test] + fn it_should_read() { + let image: UiImage = serde_xml_rs::from_str(IMAGE).unwrap(); + assert_eq!(image.texture, "tex".to_string()); + assert_eq!(image.position, [1, 2]); + assert_eq!(image.size, [3, 4]); + } +} diff --git a/rust/springylib/src/media/ui/menu.rs b/rust/springylib/src/media/ui/menu.rs new file mode 100644 index 0000000..3fb51eb --- /dev/null +++ b/rust/springylib/src/media/ui/menu.rs @@ -0,0 +1,11 @@ +use serde::Deserialize; +use crate::media::ui::UiTag; + +#[derive(Debug, Clone, Deserialize)] +pub struct UiMenu { + pub selected: String, + #[serde(rename = "OnBack")] + pub on_back: Option, + #[serde(rename = "$value", default)] + pub children: Vec, +} diff --git a/rust/springylib/src/media/ui/mod.rs b/rust/springylib/src/media/ui/mod.rs new file mode 100644 index 0000000..6ce9eb3 --- /dev/null +++ b/rust/springylib/src/media/ui/mod.rs @@ -0,0 +1,88 @@ +use crate::media::ui::image::UiImage; +use crate::media::ui::menu::UiMenu; +use crate::media::ui::static_text::UiStaticText; +use crate::media::ui::text_area::UiTextArea; +use crate::media::ui::text_button::UiTextButton; +use crate::media::ui::text_field::UiTextField; +use crate::media::ui::toggle_button::UiToggleButton; +use serde::Deserialize; + +pub mod image; +pub mod menu; +pub mod static_text; +pub mod text_area; +pub mod text_button; +pub mod text_field; +pub mod toggle_button; +pub mod vec; + +#[derive(Debug, Clone, Deserialize)] +pub enum UiTag { + Menu(UiMenu), + Image(UiImage), + TextButton(UiTextButton), + TextArea(UiTextArea), + TextField(UiTextField), + StaticText(UiStaticText), + ToggleButton(UiToggleButton), +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum HorizontalAlign { + Left, + Center, + Right, +} + +impl Default for HorizontalAlign { + fn default() -> HorizontalAlign { + HorizontalAlign::Left + } +} + +#[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum FadeMode { + None, +} + +impl Default for FadeMode { + fn default() -> Self { + FadeMode::None + } +} + +impl UiTag { + pub fn post_process(&mut self) { + if let UiTag::Menu(menu) = self { + let children: Vec = menu.children.drain(..).collect(); + let mut area_stack: Vec> = vec![vec![]]; + + for mut child in children { + child.post_process(); + if let UiTag::TextArea(mut area) = child { + let children = area_stack.pop().unwrap(); + let opening_tag = area_stack.last_mut().map(|it| it.last_mut()); + + if let Some(Some(UiTag::TextArea(opening_tag))) = opening_tag { + opening_tag.children = children; + } else { + area_stack.push(children); + } + + if area.position.is_some() && area.size.is_some() { + let children = area.children.drain(..).collect(); + area_stack.last_mut().unwrap().push(UiTag::TextArea(area)); + area_stack.push(children); + } + } else { + area_stack.last_mut().unwrap().push(child); + } + } + + menu.children = area_stack.pop().unwrap(); + debug_assert!(area_stack.is_empty()); + } + } +} diff --git a/rust/springylib/src/media/ui/static_text.rs b/rust/springylib/src/media/ui/static_text.rs new file mode 100644 index 0000000..dfec4b3 --- /dev/null +++ b/rust/springylib/src/media/ui/static_text.rs @@ -0,0 +1,29 @@ +use crate::media::ui::vec::deserialize_vec2; +use crate::media::ui::{FadeMode, HorizontalAlign}; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct UiStaticText { + pub text: String, + #[serde(deserialize_with = "deserialize_vec2")] + pub position: [i32; 2], + #[serde(rename = "halign", default)] + pub horizontal_align: HorizontalAlign, + #[serde(rename = "fademode", default)] + pub fade_mode: FadeMode, +} + +#[cfg(test)] +mod tests { + use crate::media::ui::static_text::UiStaticText; + + // language=xml + const STATIC_TEXT: &str = ""; + + #[test] + fn it_should_read() { + let static_text: UiStaticText = serde_xml_rs::from_str(STATIC_TEXT).unwrap(); + assert_eq!(static_text.text, "test".to_string()); + assert_eq!(static_text.position, [1, 2]); + } +} diff --git a/rust/springylib/src/media/ui/text_area.rs b/rust/springylib/src/media/ui/text_area.rs new file mode 100644 index 0000000..4c157bf --- /dev/null +++ b/rust/springylib/src/media/ui/text_area.rs @@ -0,0 +1,15 @@ +use crate::media::ui::vec::deserialize_vec2_opt; +use crate::media::ui::UiTag; +use serde::Deserialize; + +/// This is a really weird node, sometimes it has children and sometimes, don't ask me why, +/// it appears as a normal tag and then gets closed by an empty tag of this kind. +#[derive(Debug, Clone, Deserialize)] +pub struct UiTextArea { + #[serde(deserialize_with = "deserialize_vec2_opt", default)] + pub position: Option<[i32; 2]>, + #[serde(deserialize_with = "deserialize_vec2_opt", default)] + pub size: Option<[i32; 2]>, + #[serde(rename = "$value", default)] + pub children: Vec, +} diff --git a/rust/springylib/src/media/ui/text_button.rs b/rust/springylib/src/media/ui/text_button.rs new file mode 100644 index 0000000..5ca30b8 --- /dev/null +++ b/rust/springylib/src/media/ui/text_button.rs @@ -0,0 +1,35 @@ +use crate::media::ui::vec::deserialize_vec2; +use crate::media::ui::{FadeMode, HorizontalAlign}; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct UiTextButton { + pub name: Option, + pub text: String, + #[serde(deserialize_with = "deserialize_vec2")] + pub position: [i32; 2], + #[serde(rename = "halign", default)] + pub horizontal_align: HorizontalAlign, + #[serde(rename = "fademode", default)] + pub fade_mode: FadeMode, + #[serde(rename = "OnSelect")] + pub on_select: String, +} + +#[cfg(test)] +mod tests { + use crate::media::ui::text_button::UiTextButton; + + // language=xml + const BUTTON: &str = + ""; + + #[test] + fn it_should_read() { + let button: UiTextButton = serde_xml_rs::from_str(BUTTON).unwrap(); + assert_eq!(button.name, Some("test".to_string())); + assert_eq!(button.text, "abc".to_string()); + assert_eq!(button.position, [1, 2]); + assert_eq!(button.on_select, "StartGame".to_string()); + } +} diff --git a/rust/springylib/src/media/ui/text_field.rs b/rust/springylib/src/media/ui/text_field.rs new file mode 100644 index 0000000..6ec1633 --- /dev/null +++ b/rust/springylib/src/media/ui/text_field.rs @@ -0,0 +1,40 @@ +use crate::media::ui::vec::{deserialize_vec2, deserialize_vec4}; +use crate::media::ui::{FadeMode, HorizontalAlign}; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize)] +pub struct UiTextField { + pub name: Option, + pub text: String, + #[serde(deserialize_with = "deserialize_vec2")] + pub position: [i32; 2], + #[serde(rename = "bufferVar")] + pub buffer_var: String, + #[serde(deserialize_with = "deserialize_vec4")] + pub area: [i32; 4], + #[serde(rename = "halign", default)] + pub horizontal_align: HorizontalAlign, + #[serde(rename = "fademode", default)] + pub fade_mode: FadeMode, + #[serde(rename = "OnSelect")] + pub on_select: String, +} + +#[cfg(test)] +mod tests { + use crate::media::ui::text_field::UiTextField; + + // language=xml + const TEXT_FIELD: &str = ""; + + #[test] + fn it_should_read() { + let text_field: UiTextField = serde_xml_rs::from_str(TEXT_FIELD).unwrap(); + assert_eq!(text_field.name, Some("test".to_string())); + assert_eq!(text_field.text, "abc".to_string()); + assert_eq!(text_field.position, [1, 2]); + assert_eq!(text_field.buffer_var, "var".to_string()); + assert_eq!(text_field.area, [1, 2, 3, 4]); + assert_eq!(text_field.on_select, "click".to_string()); + } +} diff --git a/rust/springylib/src/media/ui/toggle_button.rs b/rust/springylib/src/media/ui/toggle_button.rs new file mode 100644 index 0000000..26855e8 --- /dev/null +++ b/rust/springylib/src/media/ui/toggle_button.rs @@ -0,0 +1,72 @@ +use crate::media::ui::vec::deserialize_vec2; +use serde::Deserialize; + +#[derive(Debug, Clone, Deserialize, PartialEq)] +pub struct UiToggleButton { + pub name: Option, + pub text: String, + #[serde(deserialize_with = "deserialize_vec2")] + pub position: [i32; 2], + pub value: String, + #[serde(rename = "minValue")] + pub min_value: i32, + #[serde(rename = "maxValue")] + pub max_value: i32, + #[serde(rename = "valueStep")] + pub value_step: i32, + pub target: String, + #[serde(rename = "targetLOffset", deserialize_with = "deserialize_vec2")] + pub target_l_offset: [i32; 2], + #[serde(rename = "targetROffset", deserialize_with = "deserialize_vec2")] + pub target_r_offset: [i32; 2], + #[serde(rename = "noSound", default)] + pub no_sound: bool, + #[serde(rename = "OnChange")] + pub on_change: String, + #[serde(rename = "OnSelect")] + pub on_select: String, +} + +#[cfg(test)] +mod tests { + use crate::media::ui::toggle_button::UiToggleButton; + + // language=xml + const TOGGLE_BUTTON: &str = ""; + + #[test] + fn it_should_read() { + let toggle_button: UiToggleButton = serde_xml_rs::from_str(TOGGLE_BUTTON).unwrap(); + assert_eq!( + toggle_button, + UiToggleButton { + name: Some("test".to_string()), + text: "abc".to_string(), + position: [1, 2], + value: "val".to_string(), + min_value: 0, + max_value: 10, + value_step: 1, + target: "target".to_string(), + target_l_offset: [3, 4], + target_r_offset: [5, 6], + no_sound: false, + on_change: "change".to_string(), + on_select: "select".to_string(), + } + ) + } +} diff --git a/rust/springylib/src/media/ui/vec.rs b/rust/springylib/src/media/ui/vec.rs new file mode 100644 index 0000000..0017b3d --- /dev/null +++ b/rust/springylib/src/media/ui/vec.rs @@ -0,0 +1,67 @@ +use serde::{Deserialize, Deserializer}; +use serde::de::Error; + +pub fn deserialize_vec2_opt<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, +{ + if let Some(buf) = Option::::deserialize(deserializer)? { + to_vec2::(buf).map(Some) + } else { + Ok(None) + } +} + +pub fn deserialize_vec2<'de, D>(deserializer: D) -> Result<[i32; 2], D::Error> + where + D: Deserializer<'de>, +{ + to_vec2::(String::deserialize(deserializer)?) +} + +pub fn deserialize_vec4<'de, D>(deserializer: D) -> Result<[i32; 4], D::Error> + where + D: Deserializer<'de>, +{ + to_vec4::(String::deserialize(deserializer)?) +} + +fn to_vec<'de, D>(buf: String) -> Result, D::Error> + where + D: Deserializer<'de>, +{ + buf.split(',') + .into_iter() + .map(|value| { + // there's some typos so we have to cover that... + value.split_ascii_whitespace().collect::>()[0] + .trim() + .parse::() + .map_err(|err| Error::custom(err.to_string())) + }) + .collect() +} + +fn to_vec4<'de, D>(buf: String) -> Result<[i32; 4], D::Error> + where + D: Deserializer<'de>, +{ + let mut values = to_vec::(buf)?; + let w = values.pop().ok_or(Error::custom("InvalidField"))?; + let z = values.pop().ok_or(Error::custom("InvalidField"))?; + let y = values.pop().ok_or(Error::custom("InvalidField"))?; + let x = values.pop().ok_or(Error::custom("InvalidField"))?; + + Ok([x, y, z, w]) +} + +fn to_vec2<'de, D>(buf: String) -> Result<[i32; 2], D::Error> + where + D: Deserializer<'de>, +{ + let mut values = to_vec::(buf)?; + let y = values.pop().ok_or(Error::custom("InvalidField"))?; + let x = values.pop().ok_or(Error::custom("InvalidField"))?; + + Ok([x, y]) +}