diff --git a/Cargo.lock b/Cargo.lock index 8d39bfe..c75cbbc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -542,6 +542,12 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "json" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "078e285eafdfb6c4b434e0d31e8cfcb5115b651496faca5749b88fafd4f23bfd" + [[package]] name = "lazy_static" version = "1.5.0" @@ -598,6 +604,7 @@ dependencies = [ "fancy-regex", "flate2", "int-enum", + "json", "lazy_static", "libtlg-rs", "msg_tool_macro", diff --git a/Cargo.toml b/Cargo.toml index 4b64703..b3bbe40 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ encoding_rs = "0.8" fancy-regex = { version = "0.14", optional = true } flate2 = { version = "1.1", optional = true } int-enum = { version = "1.2", optional = true } +json = { version = "0.12", optional = true } lazy_static = "1.5.0" libtlg-rs = { version = "0.1", optional = true } msg_tool_macro = { path = "./msg_tool_macro" } @@ -36,7 +37,7 @@ cat-system-img = ["cat-system", "flate2", "image", "utils-bit-stream"] circus = [] escude = ["int-enum"] escude-arc = ["escude", "rand", "utils-bit-stream"] -kirikiri = ["emote-psb", "fancy-regex", "flate2", "utils-escape"] +kirikiri = ["emote-psb", "fancy-regex", "flate2", "json", "utils-escape"] kirikiri-img = ["kirikiri", "emote-psb", "image", "libtlg-rs", "url"] yaneurao = [] yaneurao-itufuru = ["yaneurao"] diff --git a/src/ext/psb.rs b/src/ext/psb.rs index 293bfca..23f3d9f 100644 --- a/src/ext/psb.rs +++ b/src/ext/psb.rs @@ -5,6 +5,8 @@ use emote_psb::types::number::*; use emote_psb::types::reference::*; use emote_psb::types::string::*; use emote_psb::types::*; +#[cfg(feature = "json")] +use json::JsonValue; use std::cmp::PartialEq; use std::collections::HashMap; use std::ops::{Index, IndexMut}; @@ -163,6 +165,89 @@ impl PsbValueFixed { _ => None, } } + + #[cfg(feature = "json")] + pub fn to_json(&self) -> Option { + match self { + PsbValueFixed::Null => Some(JsonValue::Null), + PsbValueFixed::Bool(b) => Some(JsonValue::Boolean(*b)), + PsbValueFixed::Number(n) => match n { + PsbNumber::Integer(i) => Some(JsonValue::Number((*i).into())), + PsbNumber::Float(f) => Some(JsonValue::Number((*f).into())), + PsbNumber::Double(d) => Some(JsonValue::Number((*d).into())), + }, + PsbValueFixed::String(s) => Some(JsonValue::String(s.string().to_owned())), + PsbValueFixed::Resource(s) => { + Some(JsonValue::String(format!("resource#{}", s.resource_ref))) + } + PsbValueFixed::ExtraResource(s) => Some(JsonValue::String(format!( + "extra_resource#{}", + s.extra_resource_ref + ))), + PsbValueFixed::IntArray(arr) => Some(JsonValue::Array( + arr.iter().map(|n| JsonValue::Number((*n).into())).collect(), + )), + PsbValueFixed::List(l) => Some(l.to_json()), + PsbValueFixed::Object(o) => Some(o.to_json()), + _ => None, + } + } + + #[cfg(feature = "json")] + pub fn from_json(obj: &JsonValue) -> Self { + match obj { + JsonValue::Null => PsbValueFixed::Null, + JsonValue::Boolean(b) => PsbValueFixed::Bool(*b), + JsonValue::Number(n) => { + let data: f64 = (*n).into(); + if data.fract() == 0.0 { + PsbValueFixed::Number(PsbNumber::Integer(data as i64)) + } else { + PsbValueFixed::Number(PsbNumber::Float(data as f32)) + } + } + JsonValue::String(s) => { + if s.starts_with("resource#") { + if let Ok(id) = s[9..].parse::() { + return PsbValueFixed::Resource(PsbResourceRef { resource_ref: id }); + } + } else if s.starts_with("extra_resource#") { + if let Ok(id) = s[16..].parse::() { + return PsbValueFixed::ExtraResource(PsbExtraRef { + extra_resource_ref: id, + }); + } + } + PsbValueFixed::String(PsbString::from(s.clone())) + } + JsonValue::Array(arr) => { + let values: Vec = arr.iter().map(PsbValueFixed::from_json).collect(); + PsbValueFixed::List(PsbListFixed { values }) + } + JsonValue::Object(obj) => { + let mut values = HashMap::new(); + for (key, value) in obj.iter() { + values.insert(key.to_owned(), PsbValueFixed::from_json(value)); + } + PsbValueFixed::Object(PsbObjectFixed { values }) + } + JsonValue::Short(n) => { + let s = n.as_str(); + if s.starts_with("resource#") { + if let Ok(id) = s[9..].parse::() { + return PsbValueFixed::Resource(PsbResourceRef { resource_ref: id }); + } + } else if s.starts_with("extra_resource#") { + if let Ok(id) = s[16..].parse::() { + return PsbValueFixed::ExtraResource(PsbExtraRef { + extra_resource_ref: id, + }); + } + } + PsbValueFixed::String(PsbString::from(s.to_owned())) + } + } + } } impl Index for PsbValueFixed { @@ -351,6 +436,12 @@ impl PsbListFixed { pub fn len(&self) -> usize { self.values.len() } + + #[cfg(feature = "json")] + pub fn to_json(&self) -> JsonValue { + let data: Vec<_> = self.values.iter().filter_map(|v| v.to_json()).collect(); + JsonValue::Array(data) + } } impl Index for PsbListFixed { @@ -484,6 +575,26 @@ impl PsbObjectFixed { inner: self.values.iter_mut(), } } + + #[cfg(feature = "json")] + pub fn to_json(&self) -> JsonValue { + let mut obj = json::object::Object::new(); + for (key, value) in &self.values { + if let Some(json_value) = value.to_json() { + obj.insert(key, json_value); + } + } + JsonValue::Object(obj) + } + + #[cfg(feature = "json")] + pub fn from_json(obj: &JsonValue) -> Self { + let mut values = HashMap::new(); + for (key, value) in obj.entries() { + values.insert(key.to_owned(), PsbValueFixed::from_json(value)); + } + PsbObjectFixed { values } + } } impl<'a> Index<&'a str> for PsbObjectFixed { @@ -658,6 +769,29 @@ impl VirtualPsbFixed { let (header, resources, extra, root) = self.unwrap(); VirtualPsb::new(header, resources, extra, root.to_psb()) } + + #[cfg(feature = "json")] + pub fn from_json(&mut self, obj: &JsonValue) -> Result<(), anyhow::Error> { + let version = obj["version"] + .as_u16() + .ok_or_else(|| anyhow::anyhow!("Invalid PSB version"))?; + let encryption = obj["encryption"] + .as_u16() + .ok_or_else(|| anyhow::anyhow!("Invalid PSB encryption"))?; + self.header.version = version; + self.header.encryption = encryption; + self.root = PsbObjectFixed::from_json(&obj["data"]); + Ok(()) + } + + #[cfg(feature = "json")] + pub fn to_json(&self) -> JsonValue { + json::object! { + "version": self.header.version, + "encryption": self.header.encryption, + "data": self.root.to_json(), + } + } } pub trait VirtualPsbExt { diff --git a/src/scripts/kirikiri/scn.rs b/src/scripts/kirikiri/scn.rs index 07f4cfe..a50549a 100644 --- a/src/scripts/kirikiri/scn.rs +++ b/src/scripts/kirikiri/scn.rs @@ -2,6 +2,7 @@ use crate::ext::io::*; use crate::ext::psb::*; use crate::scripts::base::*; use crate::types::*; +use crate::utils::encoding::*; use anyhow::Result; use emote_psb::{PsbReader, PsbWriter}; use std::collections::{HashMap, HashSet}; @@ -131,6 +132,10 @@ impl Script for ScnScript { FormatOptions::None } + fn is_output_supported(&self, _: OutputScriptType) -> bool { + true + } + fn extract_messages(&self) -> Result> { let mut messages = Vec::new(); let root = self.psb.root(); @@ -566,6 +571,34 @@ impl Script for ScnScript { })?; Ok(()) } + + fn custom_export(&self, filename: &Path, encoding: Encoding) -> Result<()> { + let s = json::stringify_pretty(self.psb.to_json(), 2); + let mut f = crate::utils::files::write_file(filename)?; + let b = encode_string(encoding, &s, false)?; + f.write_all(&b)?; + Ok(()) + } + + fn custom_import<'a>( + &'a self, + custom_filename: &'a str, + file: Box, + _encoding: Encoding, + output_encoding: Encoding, + ) -> Result<()> { + let data = crate::utils::files::read_file(custom_filename)?; + let s = decode_to_string(output_encoding, &data)?; + let json = json::parse(&s)?; + let mut psb = self.psb.clone(); + psb.from_json(&json)?; + let psb = psb.to_psb(); + let writer = PsbWriter::new(psb, file); + writer.finish().map_err(|e| { + anyhow::anyhow!("Failed to write PSB to file {}: {:?}", self.filename, e) + })?; + Ok(()) + } } #[derive(Debug)]