From cbfbf622cf9a3d991a936a3a8f37bcf54f720e4f Mon Sep 17 00:00:00 2001 From: lifegpc Date: Tue, 1 Jul 2025 11:14:40 +0800 Subject: [PATCH] Add support to import scn script --- Cargo.lock | 7 + Cargo.toml | 5 +- src/scripts/kirikiri/scn.rs | 249 +++++++++++++++++++++++++++++++++++- 3 files changed, 254 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31ff056..3163723 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -381,6 +381,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" @@ -421,6 +427,7 @@ dependencies = [ "encoding_rs", "flate2", "int-enum", + "json", "lazy_static", "msg_tool_macro", "overf", diff --git a/Cargo.toml b/Cargo.toml index 8276267..5e17d9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,10 +8,11 @@ anyhow = "1" blowfish = { version = "0.9", optional = true } clap = { version = "4.5", features = ["derive"] } csv = "1.3" -emote-psb = { version = "0.5.0", features = ["serde"], optional = true } +emote-psb = { version = "0.5", features = ["serde"], optional = true } encoding_rs = "0.8" flate2 = { version = "1.1", optional = true } int-enum = { version = "1.2", optional = true } +json = { version = "0.12", optional = true } lazy_static = "1.5.0" msg_tool_macro = { path = "./msg_tool_macro" } overf = "0.1" @@ -32,7 +33,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"] +kirikiri = ["emote-psb", "json"] yaneurao = [] yaneurao-itufuru = ["yaneurao"] # basic feature diff --git a/src/scripts/kirikiri/scn.rs b/src/scripts/kirikiri/scn.rs index e82a8a5..3613941 100644 --- a/src/scripts/kirikiri/scn.rs +++ b/src/scripts/kirikiri/scn.rs @@ -4,12 +4,24 @@ use crate::types::*; use crate::utils::encoding::{decode_to_string, encode_string}; use anyhow::Result; use emote_psb::types::PsbValue; +use emote_psb::types::collection::PsbObject; use emote_psb::{PsbReader, PsbWriter, VirtualPsb}; +use json::JsonValue; use serde::{Deserialize, Serialize}; use std::collections::HashSet; use std::io::{Read, Seek, Write}; use std::path::Path; +trait JsonExt { + fn is_valid_str(&self) -> bool; +} + +impl JsonExt for JsonValue { + fn is_valid_str(&self) -> bool { + self.is_string() || self.is_null() + } +} + #[derive(Debug)] pub struct ScnScriptBuilder {} @@ -189,7 +201,7 @@ impl Script for ScnScript { _ => return Err(anyhow::anyhow!("scene at index {} is not an object", i)), }; if let Some(PsbValue::List(texts)) = scene.get_value("texts".into()) { - for text in texts.iter() { + for (j, text) in texts.iter().enumerate() { if let PsbValue::List(text) = text { let values = text.values(); if values.len() <= 1 { @@ -199,6 +211,7 @@ impl Script for ScnScript { let name = match name { PsbValue::String(s) => Some(s), PsbValue::Null => None, + PsbValue::None => None, _ => return Err(anyhow::anyhow!("name is not a string or null")), }; let mut display_name; @@ -213,9 +226,10 @@ impl Script for ScnScript { display_name = match &values[1] { PsbValue::String(s) => Some(s), PsbValue::Null => None, + PsbValue::None => None, _ => { return Err(anyhow::anyhow!( - "display name is not a string or null" + "display name is not a string or null at {i},{j}" )); } }; @@ -233,9 +247,10 @@ impl Script for ScnScript { display_name = match &data[0] { PsbValue::String(s) => Some(s), PsbValue::Null => None, + PsbValue::None => None, _ => { return Err(anyhow::anyhow!( - "display name is not a string or null" + "display name is not a string or null at {i},{j}" )); } }; @@ -282,6 +297,7 @@ impl Script for ScnScript { text = match v.get_value("text".into()) { Some(PsbValue::String(s)) => Some(s), Some(PsbValue::Null) => None, + Some(PsbValue::None) => None, None => None, _ => { return Err(anyhow::anyhow!( @@ -296,6 +312,7 @@ impl Script for ScnScript { text = match select.get_value("text".into()) { Some(PsbValue::String(s)) => Some(s), Some(PsbValue::Null) => None, + Some(PsbValue::None) => None, None => None, _ => { return Err(anyhow::anyhow!( @@ -346,6 +363,227 @@ impl Script for ScnScript { Ok(messages) } + fn import_messages<'a>( + &'a self, + messages: Vec, + file: Box, + _encoding: Encoding, + replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + let mut mes = messages.iter(); + let mut cur_mes = mes.next(); + // We use json library to process the PSB data, because emote-psb does not support update data. + let t = serde_json::to_string(&self.psb.root())?; + let mut root = json::parse(&t)?; + let scenes = &mut root["scenes"]; + if !scenes.is_array() { + return Err(anyhow::anyhow!("scenes is not an array")); + } + for (i, scene) in scenes.members_mut().enumerate() { + if !scene.is_object() { + return Err(anyhow::anyhow!("scene at {} is not an object", i)); + } + for text in scene["texts"].members_mut() { + if text.is_array() { + if text.len() <= 1 { + continue; // Skip if there are not enough values + } + if cur_mes.is_none() { + cur_mes = mes.next(); + } + if !text[0].is_valid_str() { + return Err(anyhow::anyhow!("name is not a string or null")); + } + let has_name = text[0].is_string(); + let mut has_display_name; + if text[1].is_array() { + if text[1].is_string() { + let m = match cur_mes.take() { + Some(m) => m, + None => { + return Err(anyhow::anyhow!("No enough messages.")); + } + }; + if has_name { + if let Some(name) = &m.name { + let mut name = name.clone(); + if let Some(replacement) = replacement { + for (key, value) in replacement.map.iter() { + name = name.replace(key, value); + } + } + text[0] = json::JsonValue::String(name); + } else { + return Err(anyhow::anyhow!("Name is missing for message.")); + } + } + let mut message = m.message.clone(); + if let Some(replacement) = replacement { + for (key, value) in replacement.map.iter() { + message = message.replace(key, value); + } + } + text[1] = json::JsonValue::String(message.replace("\n", "\\n")); + } else if text[1].is_array() { + if text[1].len() > self.language_index + && text[1][self.language_index].is_array() + && text[1][self.language_index].len() >= 2 + { + if !text[1][self.language_index][0].is_valid_str() { + return Err(anyhow::anyhow!( + "display name is not a string or null" + )); + } + has_display_name = text[1][self.language_index][0].is_string(); + if text[1][self.language_index][1].is_string() { + let m = match cur_mes.take() { + Some(m) => m, + None => { + return Err(anyhow::anyhow!("No enough messages.")); + } + }; + if has_name { + if let Some(name) = &m.name { + let mut name = name.clone(); + if let Some(replacement) = replacement { + for (key, value) in replacement.map.iter() { + name = name.replace(key, value); + } + } + if has_display_name { + text[1][self.language_index][0] = + json::JsonValue::String(name); + } else { + text[0] = json::JsonValue::String(name); + } + } else { + return Err(anyhow::anyhow!( + "Name is missing for message." + )); + } + } + let mut message = m.message.clone(); + if let Some(replacement) = replacement { + for (key, value) in replacement.map.iter() { + message = message.replace(key, value); + } + } + text[1][self.language_index][1] = + json::JsonValue::String(message.replace("\n", "\\n")); + } + } + } + } else { + if text.len() <= 2 { + continue; // Skip if there is no message + } + if !text[1].is_valid_str() { + return Err(anyhow::anyhow!("display name is not a string or null")); + } + has_display_name = text[1].is_string(); + if text[2].is_string() { + let m = match cur_mes.take() { + Some(m) => m, + None => { + return Err(anyhow::anyhow!("No enough messages.")); + } + }; + if has_name { + if let Some(name) = &m.name { + let mut name = name.clone(); + if let Some(replacement) = replacement { + for (key, value) in replacement.map.iter() { + name = name.replace(key, value); + } + } + if has_display_name { + text[1] = json::JsonValue::String(name); + } else { + text[0] = json::JsonValue::String(name); + } + } else { + return Err(anyhow::anyhow!("Name is missing for message.")); + } + } + let mut message = m.message.clone(); + if let Some(replacement) = replacement { + for (key, value) in replacement.map.iter() { + message = message.replace(key, value); + } + } + text[2] = json::JsonValue::String(message.replace("\n", "\\n")); + } else if text[2].is_array() { + if text[2].len() > self.language_index + && text[2][self.language_index].is_array() + && text[2][self.language_index].len() >= 2 + { + if !text[2][self.language_index][0].is_valid_str() { + return Err(anyhow::anyhow!( + "display name is not a string or null" + )); + } + has_display_name = text[2][self.language_index][0].is_string(); + if text[2][self.language_index][1].is_string() { + let m = match cur_mes.take() { + Some(m) => m, + None => { + return Err(anyhow::anyhow!("No enough messages.")); + } + }; + if has_name { + if let Some(name) = &m.name { + let mut name = name.clone(); + if let Some(replacement) = replacement { + for (key, value) in replacement.map.iter() { + name = name.replace(key, value); + } + } + if has_display_name { + text[2][self.language_index][0] = + json::JsonValue::String(name); + } else { + text[0] = json::JsonValue::String(name); + } + } else { + return Err(anyhow::anyhow!( + "Name is missing for message." + )); + } + } + let mut message = m.message.clone(); + if let Some(replacement) = replacement { + for (key, value) in replacement.map.iter() { + message = message.replace(key, value); + } + } + text[2][self.language_index][1] = + json::JsonValue::String(message.replace("\n", "\\n")); + } + } + } + } + } + } + // #TODO: selects and comumode + } + if cur_mes.is_some() || mes.next().is_some() { + return Err(anyhow::anyhow!("Some messages were not processed.")); + } + let s = json::stringify(root); + let obj = serde_json::from_str::(&s)?; + let oheader = self.psb.header(); + let header = emote_psb::header::PsbHeader { + version: oheader.version, + encryption: oheader.encryption, + }; + let psb = VirtualPsb::new(header, Vec::new(), Vec::new(), obj); + let writer = PsbWriter::new(psb, file); + writer + .finish() + .map_err(|e| anyhow::anyhow!("Failed to write PSB: {:?}", e))?; + Ok(()) + } + fn custom_output_extension(&self) -> &'static str { "json" } @@ -412,7 +650,7 @@ impl ExportComuMes { if let PsbValue::Object(obj) = item { if let Some(PsbValue::String(s)) = obj.get_value("text".into()) { - self.messages.insert(s.string().to_owned()); + self.messages.insert(s.string().replace("\\n", "\n")); } } } @@ -431,7 +669,8 @@ impl ExportComuMes { if let PsbValue::String(s) = &list[i - 1] { if s.string() == "text" { if let PsbValue::String(text) = &list[i] { - self.messages.insert(text.string().to_owned()); + self.messages + .insert(text.string().replace("\\n", "\n")); } } }