use std::fmt::Write;
use ltk_meta::{
property::{
values::{Embedded, Struct, UnorderedContainer},
PropertyValueEnum,
},
Bin, BinObject, BinProperty,
};
use crate::{
error::WriteError,
hashes::{HashMapProvider, HashProvider, HexHashProvider},
types::kind_to_type_name,
};
#[derive(Debug, Clone)]
pub struct WriterConfig {
pub indent_size: usize,
}
impl Default for WriterConfig {
fn default() -> Self {
Self { indent_size: 4 }
}
}
pub struct TextWriter<'a, H: HashProvider = HexHashProvider> {
buffer: String,
indent_level: usize,
config: WriterConfig,
hashes: &'a H,
}
impl<'a> TextWriter<'a, HexHashProvider> {
pub fn new() -> Self {
static HEX_PROVIDER: HexHashProvider = HexHashProvider;
Self {
buffer: String::new(),
indent_level: 0,
config: WriterConfig::default(),
hashes: &HEX_PROVIDER,
}
}
}
impl<'a, H: HashProvider> TextWriter<'a, H> {
pub fn with_hashes(hashes: &'a H) -> Self {
Self {
buffer: String::new(),
indent_level: 0,
config: WriterConfig::default(),
hashes,
}
}
pub fn with_config_and_hashes(config: WriterConfig, hashes: &'a H) -> Self {
Self {
buffer: String::new(),
indent_level: 0,
config,
hashes,
}
}
pub fn into_string(self) -> String {
self.buffer
}
pub fn as_str(&self) -> &str {
&self.buffer
}
fn indent(&mut self) {
self.indent_level += self.config.indent_size;
}
fn dedent(&mut self) {
self.indent_level = self.indent_level.saturating_sub(self.config.indent_size);
}
fn pad(&mut self) {
for _ in 0..self.indent_level {
self.buffer.push(' ');
}
}
fn write_raw(&mut self, s: &str) {
self.buffer.push_str(s);
}
fn write_type(&mut self, value: &PropertyValueEnum) {
let type_name = kind_to_type_name(value.kind());
self.write_raw(type_name);
match value {
PropertyValueEnum::Container(container)
| PropertyValueEnum::UnorderedContainer(UnorderedContainer(container)) => {
self.write_raw("[");
self.write_raw(kind_to_type_name(container.item_kind()));
self.write_raw("]");
}
PropertyValueEnum::Optional(optional) => {
self.write_raw("[");
self.write_raw(kind_to_type_name(optional.item_kind()));
self.write_raw("]");
}
PropertyValueEnum::Map(map) => {
self.write_raw("[");
self.write_raw(kind_to_type_name(map.key_kind()));
self.write_raw(",");
self.write_raw(kind_to_type_name(map.value_kind()));
self.write_raw("]");
}
_ => {}
}
}
fn write_entry_hash(&mut self, hash: u32) -> Result<(), WriteError> {
if let Some(name) = self.hashes.lookup_entry(hash) {
write!(self.buffer, "{:?}", name)?;
} else {
write!(self.buffer, "{:#x}", hash)?;
}
Ok(())
}
fn write_field_hash(&mut self, hash: u32) -> Result<(), WriteError> {
if let Some(name) = self.hashes.lookup_field(hash) {
self.write_raw(name);
} else {
write!(self.buffer, "{:#x}", hash)?;
}
Ok(())
}
fn write_hash_value(&mut self, hash: u32) -> Result<(), WriteError> {
if let Some(name) = self.hashes.lookup_hash(hash) {
write!(self.buffer, "{:?}", name)?;
} else {
write!(self.buffer, "{:#x}", hash)?;
}
Ok(())
}
fn write_type_hash(&mut self, hash: u32) -> Result<(), WriteError> {
if let Some(name) = self.hashes.lookup_type(hash) {
self.write_raw(name);
} else {
write!(self.buffer, "{:#x}", hash)?;
}
Ok(())
}
fn write_link_hash(&mut self, hash: u32) -> Result<(), WriteError> {
if let Some(name) = self.hashes.lookup_entry(hash) {
write!(self.buffer, "{:?}", name)?;
} else {
write!(self.buffer, "{:#x}", hash)?;
}
Ok(())
}
fn write_value(&mut self, value: &PropertyValueEnum) -> Result<(), WriteError> {
match value {
PropertyValueEnum::None(_) => self.write_raw("null"),
PropertyValueEnum::Bool(v) => self.write_raw(if **v { "true" } else { "false" }),
PropertyValueEnum::I8(v) => write!(self.buffer, "{}", v.value)?,
PropertyValueEnum::U8(v) => write!(self.buffer, "{}", v.value)?,
PropertyValueEnum::I16(v) => write!(self.buffer, "{}", v.value)?,
PropertyValueEnum::U16(v) => write!(self.buffer, "{}", v.value)?,
PropertyValueEnum::I32(v) => write!(self.buffer, "{}", v.value)?,
PropertyValueEnum::U32(v) => write!(self.buffer, "{}", v.value)?,
PropertyValueEnum::I64(v) => write!(self.buffer, "{}", v.value)?,
PropertyValueEnum::U64(v) => write!(self.buffer, "{}", v.value)?,
PropertyValueEnum::F32(v) => write!(self.buffer, "{}", v.value)?,
PropertyValueEnum::Vector2(v) => {
write!(self.buffer, "{{ {}, {} }}", v.value.x, v.value.y)?;
}
PropertyValueEnum::Vector3(v) => {
write!(
self.buffer,
"{{ {}, {}, {} }}",
v.value.x, v.value.y, v.value.z
)?;
}
PropertyValueEnum::Vector4(v) => {
write!(
self.buffer,
"{{ {}, {}, {}, {} }}",
v.value.x, v.value.y, v.value.z, v.value.w
)?;
}
PropertyValueEnum::Matrix44(v) => {
self.write_raw("{\n");
self.indent();
let arr = v.value.to_cols_array();
for (i, val) in arr.iter().enumerate() {
if i % 4 == 0 {
self.pad();
}
write!(self.buffer, "{}", val)?;
if i % 4 == 3 {
self.write_raw("\n");
if i == 15 {
self.dedent();
}
} else {
self.write_raw(", ");
}
}
self.pad();
self.write_raw("}");
}
PropertyValueEnum::Color(v) => {
write!(
self.buffer,
"{{ {}, {}, {}, {} }}",
v.value.r, v.value.g, v.value.b, v.value.a
)?;
}
PropertyValueEnum::String(v) => {
write!(self.buffer, "{:?}", v.value)?;
}
PropertyValueEnum::Hash(v) => {
self.write_hash_value(v.value)?;
}
PropertyValueEnum::WadChunkLink(v) => {
write!(self.buffer, "{:#x}", v.value)?;
}
PropertyValueEnum::ObjectLink(v) => {
self.write_link_hash(v.value)?;
}
PropertyValueEnum::BitBool(v) => self.write_raw(if v.value { "true" } else { "false" }),
PropertyValueEnum::Container(container)
| PropertyValueEnum::UnorderedContainer(UnorderedContainer(container)) => {
let items = container.clone().into_items().collect::<Vec<_>>();
if items.is_empty() {
self.write_raw("{}");
} else {
self.write_raw("{\n");
self.indent();
for item in items {
self.pad();
self.write_value(&item)?;
self.write_raw("\n");
}
self.dedent();
self.pad();
self.write_raw("}");
}
}
PropertyValueEnum::Optional(value) => {
if let Some(inner) = value.clone().into_inner() {
self.write_raw("{\n");
self.indent();
self.pad();
self.write_value(&inner)?;
self.write_raw("\n");
self.dedent();
self.pad();
self.write_raw("}");
} else {
self.write_raw("{}");
}
}
PropertyValueEnum::Map(map) => {
let entries = map.entries();
if entries.is_empty() {
self.write_raw("{}");
} else {
self.write_raw("{\n");
self.indent();
for (key, value) in entries {
self.pad();
self.write_value(key)?;
self.write_raw(" = ");
self.write_value(value)?;
self.write_raw("\n");
}
self.dedent();
self.pad();
self.write_raw("}");
}
}
PropertyValueEnum::Struct(v) => {
self.write_struct_value(v)?;
}
PropertyValueEnum::Embedded(Embedded(v)) => {
self.write_struct_value(v)?;
}
}
Ok(())
}
fn write_struct_value(&mut self, v: &Struct) -> Result<(), WriteError> {
if v.class_hash == 0 && v.properties.is_empty() {
self.write_raw("null");
} else {
self.write_type_hash(v.class_hash)?;
self.write_raw(" ");
if v.properties.is_empty() {
self.write_raw("{}");
} else {
self.write_raw("{\n");
self.indent();
for prop in v.properties.values() {
self.write_property(prop)?;
}
self.dedent();
self.pad();
self.write_raw("}");
}
}
Ok(())
}
fn write_property(&mut self, prop: &BinProperty) -> Result<(), WriteError> {
self.pad();
self.write_field_hash(prop.name_hash)?;
self.write_raw(": ");
self.write_type(&prop.value);
self.write_raw(" = ");
self.write_value(&prop.value)?;
self.write_raw("\n");
Ok(())
}
pub fn write_tree(&mut self, tree: &Bin) -> Result<(), WriteError> {
self.write_raw("#PROP_text\n");
self.write_raw("type: string = \"PROP\"\n");
writeln!(self.buffer, "version: u32 = {}", tree.version)?;
if !tree.dependencies.is_empty() {
self.write_raw("linked: list[string] = {\n");
self.indent();
for dep in &tree.dependencies {
self.pad();
writeln!(self.buffer, "{:?}", dep)?;
}
self.dedent();
self.write_raw("}\n");
}
if !tree.objects.is_empty() {
self.write_raw("entries: map[hash,embed] = {\n");
self.indent();
for obj in tree.objects.values() {
self.write_object(obj)?;
}
self.dedent();
self.write_raw("}\n");
}
Ok(())
}
fn write_object(&mut self, obj: &BinObject) -> Result<(), WriteError> {
self.pad();
self.write_entry_hash(obj.path_hash)?;
self.write_raw(" = ");
self.write_type_hash(obj.class_hash)?;
self.write_raw(" ");
if obj.properties.is_empty() {
self.write_raw("{}\n");
} else {
self.write_raw("{\n");
self.indent();
for prop in obj.properties.values() {
self.write_property(prop)?;
}
self.dedent();
self.pad();
self.write_raw("}\n");
}
Ok(())
}
}
impl Default for TextWriter<'_, HexHashProvider> {
fn default() -> Self {
Self::new()
}
}
pub fn write(tree: &Bin) -> Result<String, WriteError> {
let mut writer = TextWriter::new();
writer.write_tree(tree)?;
Ok(writer.into_string())
}
pub fn write_with_config(tree: &Bin, config: WriterConfig) -> Result<String, WriteError> {
static HEX_PROVIDER: HexHashProvider = HexHashProvider;
let mut writer = TextWriter::with_config_and_hashes(config, &HEX_PROVIDER);
writer.write_tree(tree)?;
Ok(writer.into_string())
}
pub fn write_with_hashes<H: HashProvider>(tree: &Bin, hashes: &H) -> Result<String, WriteError> {
let mut writer = TextWriter::with_hashes(hashes);
writer.write_tree(tree)?;
Ok(writer.into_string())
}
pub fn write_with_config_and_hashes<H: HashProvider>(
tree: &Bin,
config: WriterConfig,
hashes: &H,
) -> Result<String, WriteError> {
let mut writer = TextWriter::with_config_and_hashes(config, hashes);
writer.write_tree(tree)?;
Ok(writer.into_string())
}
#[derive(Debug, Default, Clone)]
pub struct RitobinBuilder {
is_override: bool,
dependencies: Vec<String>,
objects: Vec<BinObject>,
}
impl RitobinBuilder {
pub fn new() -> Self {
Self::default()
}
pub fn is_override(mut self, is_override: bool) -> Self {
self.is_override = is_override;
self
}
pub fn dependency(mut self, dep: impl Into<String>) -> Self {
self.dependencies.push(dep.into());
self
}
pub fn dependencies(mut self, deps: impl IntoIterator<Item = impl Into<String>>) -> Self {
self.dependencies.extend(deps.into_iter().map(Into::into));
self
}
pub fn object(mut self, obj: BinObject) -> Self {
self.objects.push(obj);
self
}
pub fn objects(mut self, objs: impl IntoIterator<Item = BinObject>) -> Self {
self.objects.extend(objs);
self
}
pub fn build(self) -> Bin {
Bin::builder()
.is_override(self.is_override)
.dependencies(self.dependencies)
.objects(self.objects)
.build()
}
pub fn to_text(self) -> Result<String, WriteError> {
write(&self.build())
}
pub fn to_text_with_hashes<H: HashProvider>(self, hashes: &H) -> Result<String, WriteError> {
write_with_hashes(&self.build(), hashes)
}
}
pub type HexWriter<'a> = TextWriter<'a, HexHashProvider>;
pub type NamedWriter<'a> = TextWriter<'a, HashMapProvider>;
#[cfg(test)]
mod tests {
use super::*;
use crate::hashes::HashMapProvider;
#[test]
fn test_write_simple() {
let tree = Bin::new([], std::iter::empty::<&str>());
let text = write(&tree).unwrap();
assert!(text.contains("#PROP_text"));
assert!(text.contains("type: string = \"PROP\""));
assert!(text.contains("version: u32 = 3"));
}
#[test]
fn test_write_with_dependencies() {
let tree = Bin::new(
std::iter::empty(),
vec![
"path/to/dep1.bin".to_string(),
"path/to/dep2.bin".to_string(),
],
);
let text = write(&tree).unwrap();
assert!(text.contains("linked: list[string] = {"));
assert!(text.contains("\"path/to/dep1.bin\""));
assert!(text.contains("\"path/to/dep2.bin\""));
}
#[test]
fn test_builder() {
let tree = RitobinBuilder::new().dependency("path/to/dep.bin").build();
assert_eq!(tree.dependencies.len(), 1);
assert_eq!(tree.version, 3); }
#[test]
fn test_write_with_hash_lookup() {
use indexmap::IndexMap;
use ltk_meta::property::values::String;
let mut properties = IndexMap::new();
let name_hash = ltk_hash::fnv1a::hash_lower("testField");
properties.insert(
name_hash,
BinProperty {
name_hash,
value: PropertyValueEnum::String(String::from("hello")),
},
);
let path_hash = ltk_hash::fnv1a::hash_lower("Test/Path");
let class_hash = ltk_hash::fnv1a::hash_lower("TestClass");
let obj = BinObject {
path_hash,
class_hash,
properties,
};
let tree = Bin::new(std::iter::once(obj), std::iter::empty::<&str>());
let text_hex = write(&tree).unwrap();
assert!(text_hex.contains(&format!("{:#x}", path_hash)));
let mut hashes = HashMapProvider::new();
hashes.insert_entry(path_hash, "Test/Path");
hashes.insert_field(name_hash, "testField");
hashes.insert_type(class_hash, "TestClass");
let text_named = write_with_hashes(&tree, &hashes).unwrap();
assert!(text_named.contains("\"Test/Path\""));
assert!(text_named.contains("testField:"));
assert!(text_named.contains("TestClass {"));
}
}