From 49be9a8a58a21df4a80751ecb27b97c751ccd4e1 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Thu, 16 Oct 2025 09:45:11 +0800 Subject: [PATCH] Add support to insert a new language to kirikiri scn script --- src/args.rs | 4 ++ src/ext/psb.rs | 62 ++++++++++++++++ src/main.rs | 2 + src/scripts/kirikiri/scn.rs | 140 +++++++++++++++++++++++++++++++----- src/types.rs | 3 + 5 files changed, 195 insertions(+), 16 deletions(-) diff --git a/src/args.rs b/src/args.rs index be7810f..0a5a5ef 100644 --- a/src/args.rs +++ b/src/args.rs @@ -525,6 +525,10 @@ pub struct Arg { /// Workers count for packing file in Kirikiri XP3 archive in parallel. /// This not works when segment is disabled. pub xp3_pack_workers: usize, + #[cfg(feature = "kirikiri")] + #[arg(long, global = true)] + /// Insert new language at the specified index in Kirikiri SCN script. If index is out of bounds, this flags will be ignored. + pub kirikiri_language_insert: bool, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/ext/psb.rs b/src/ext/psb.rs index c9cb61b..c8619e0 100644 --- a/src/ext/psb.rs +++ b/src/ext/psb.rs @@ -85,6 +85,26 @@ impl From for PsbValueFixed { } } +impl From for PsbValueFixed { + fn from(value: PsbObjectFixed) -> Self { + PsbValueFixed::Object(value) + } +} + +impl From for PsbValueFixed { + fn from(value: PsbListFixed) -> Self { + PsbValueFixed::List(value) + } +} + +impl From<&[PsbValueFixed]> for PsbValueFixed { + fn from(value: &[PsbValueFixed]) -> Self { + PsbValueFixed::List(PsbListFixed { + values: value.to_vec(), + }) + } +} + impl PsbValueFixed { /// Converts this value to original PSB value type. pub fn to_psb(self, warn_on_none: bool) -> PsbValue { @@ -284,6 +304,33 @@ impl PsbValueFixed { } } + /// Clears all members in a list. If this value is not a list, it will be converted to an empty list. + pub fn clear_members(&mut self) { + match self { + PsbValueFixed::List(l) => { + l.clear(); + } + _ => { + *self = PsbValueFixed::List(PsbListFixed { values: vec![] }); + } + } + } + + /// Inserts a new member at the specified index in a list. If this value is not a list, it will be converted to a list. + /// If the index is out of bounds, the value will be appended to the end of the list. + pub fn insert_member>(&mut self, index: usize, value: T) { + match self { + PsbValueFixed::List(l) => { + l.insert(index, value); + } + _ => { + *self = PsbValueFixed::List(PsbListFixed { + values: vec![value.into()], + }); + } + } + } + /// Returns the resource ID if this value is a resource reference. pub fn resource_id(&self) -> Option { match self { @@ -615,6 +662,21 @@ impl PsbListFixed { self.values.len() } + /// Clears all values in the list. + pub fn clear(&mut self) { + self.values.clear(); + } + + /// Inserts a new value at the specified index in the list. + /// If the index is out of bounds, the value will be appended to the end of the list. + pub fn insert>(&mut self, index: usize, value: V) { + if index <= self.values.len() { + self.values.insert(index, value.into()); + } else { + self.values.push(value.into()); + } + } + /// Converts this PSB list to a JSON value. #[cfg(feature = "json")] pub fn to_json(&self) -> JsonValue { diff --git a/src/main.rs b/src/main.rs index 30979e6..83706b1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2762,6 +2762,8 @@ fn main() { xp3_zstd: arg.xp3_zstd, #[cfg(feature = "kirikiri-arc")] xp3_pack_workers: arg.xp3_pack_workers, + #[cfg(feature = "kirikiri")] + kirikiri_language_insert: arg.kirikiri_language_insert, }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/kirikiri/scn.rs b/src/scripts/kirikiri/scn.rs index 3dfdaeb..61a54e0 100644 --- a/src/scripts/kirikiri/scn.rs +++ b/src/scripts/kirikiri/scn.rs @@ -119,6 +119,7 @@ pub struct ScnScript { custom_yaml: bool, title: bool, chat_multilang: bool, + insert_language: bool, } impl ScnScript { @@ -157,6 +158,7 @@ impl ScnScript { custom_yaml: config.custom_yaml, title: config.kirikiri_title, chat_multilang: config.kirikiri_chat_multilang, + insert_language: config.kirikiri_language_insert, }) } } @@ -439,6 +441,15 @@ impl Script for ScnScript { } else { None }; + let ori_lang = if self.insert_language && self.language_index == 0 { + if let Some(lang) = root["languages"][0].as_str() { + Some(lang.to_owned()) + } else { + None + } + } else { + None + }; let scenes = &mut root["scenes"]; if !scenes.is_list() { return Err(anyhow::anyhow!("scenes is not an array")); @@ -454,6 +465,11 @@ impl Script for ScnScript { None }, self.filename.clone(), + if self.chat_multilang { + ori_lang.clone() + } else { + None + }, ) }); for (i, scene) in scenes.members_mut().enumerate() { @@ -477,13 +493,23 @@ impl Script for ScnScript { } } if self.language_index == 0 { - scene["title"].set_string(title); + if self.insert_language { + let ori_title = scene["title"].as_str().unwrap_or("").to_string(); + scene["title"].push_member(title); + scene["title"].push_member(ori_title); + } else { + scene["title"].set_string(title); + } } else { let ori_title = scene["title"].as_str().unwrap_or("").to_string(); while scene["title"].len() < self.language_index { scene["title"].push_member(ori_title.clone()); } - scene["title"].push_member(title); + if self.insert_language { + scene["title"].insert_member(self.language_index, title); + } else { + scene["title"].push_member(title); + } } cur_mes = mes.next(); } else if scene["title"].is_list() { @@ -502,10 +528,17 @@ impl Script for ScnScript { } } let ori_title = scene["title"][0].as_str().unwrap_or("").to_string(); - while scene["title"].len() <= self.language_index { - scene["title"].push_member(ori_title.clone()); + if self.insert_language { + while scene["title"].len() < self.language_index { + scene["title"].push_member(ori_title.clone()); + } + scene["title"].insert_member(self.language_index, title); + } else { + while scene["title"].len() <= self.language_index { + scene["title"].push_member(ori_title.clone()); + } + scene["title"][self.language_index].set_string(title); } - scene["title"][self.language_index].set_string(title); cur_mes = mes.next(); } } @@ -524,8 +557,16 @@ impl Script for ScnScript { let has_name = text[0].is_string(); let has_display_name; if text[1].is_list() { - while text[1].len() <= self.language_index { - text[1][self.language_index] = text[1][0].clone(); + if self.insert_language { + let ori = text[1][0].clone(); + while text[1].len() < self.language_index { + text[1].push_member(ori.clone()); + } + text[1].insert_member(self.language_index, ori.clone()); + } else { + while text[1].len() <= self.language_index { + text[1][self.language_index] = text[1][0].clone(); + } } if text[1][self.language_index].is_list() && text[1][self.language_index].len() >= 2 @@ -624,8 +665,16 @@ impl Script for ScnScript { } text[2].set_string(message.replace("\n", "\\n")); } else if text[2].is_list() { - while text[2].len() <= self.language_index { - text[2][self.language_index] = text[2][0].clone(); + if self.insert_language { + let ori = text[2][0].clone(); + while text[2].len() < self.language_index { + text[2].push_member(ori.clone()); + } + text[2].insert_member(self.language_index, ori.clone()); + } else { + while text[2].len() <= self.language_index { + text[2][self.language_index] = text[2][0].clone(); + } } if text[2][self.language_index].is_list() && text[2][self.language_index].len() >= 2 @@ -691,19 +740,42 @@ impl Script for ScnScript { } if self.language_index != 0 && { - while select["language"].len() <= self.language_index { - // TenShiSouZou - // first block is null - if select["language"].len() == 0 { - select["language"].push_member(PsbValueFixed::Null); - continue; + if self.insert_language { + while select["language"].len() < self.language_index { + // TenShiSouZou + // first block is null + if select["language"].len() == 0 { + select["language"].push_member(PsbValueFixed::Null); + continue; + } + let mut obj = PsbObjectFixed::new(); + obj["text"].set_str(""); + obj["speechtext"].set_str(""); + obj["searchtext"].set_str(""); + obj["textlength"].set_i64(0); + select["language"][self.language_index].set_obj(obj); } let mut obj = PsbObjectFixed::new(); obj["text"].set_str(""); obj["speechtext"].set_str(""); obj["searchtext"].set_str(""); obj["textlength"].set_i64(0); - select["language"][self.language_index].set_obj(obj); + select["language"].insert_member(self.language_index, obj); + } else { + while select["language"].len() <= self.language_index { + // TenShiSouZou + // first block is null + if select["language"].len() == 0 { + select["language"].push_member(PsbValueFixed::Null); + continue; + } + let mut obj = PsbObjectFixed::new(); + obj["text"].set_str(""); + obj["speechtext"].set_str(""); + obj["searchtext"].set_str(""); + obj["textlength"].set_i64(0); + select["language"][self.language_index].set_obj(obj); + } } true } @@ -742,6 +814,18 @@ impl Script for ScnScript { text = text.replace(key, value); } } + if self.insert_language { + let ori_text = select["text"].as_str().unwrap_or("").to_string(); + let mut obj = PsbObjectFixed::new(); + obj["text"].set_string(ori_text.replace("\n", "\\n")); + obj["speechtext"].set_string(get_save_message(&ori_text, true)); + obj["searchtext"].set_string(get_save_message(&ori_text, false)); + obj["textlength"].set_i64(ori_text.chars().count() as i64); + if select["language"].len() < 1 { + select["language"].push_member(PsbValueFixed::Null); + } + select["language"].insert_member(1, obj); + } select["text"].set_string(text.replace("\n", "\\n")); } } @@ -918,6 +1002,7 @@ struct ImportMes<'a> { key: String, text_key: String, filename: String, + ori_text_key: Option, } impl<'a> ImportMes<'a> { @@ -927,6 +1012,7 @@ impl<'a> ImportMes<'a> { key: String, lang: Option, filename: String, + ori_lang: Option, ) -> Self { Self { messages, @@ -937,6 +1023,7 @@ impl<'a> ImportMes<'a> { .file_stem() .map(|s| s.to_string_lossy().to_string()) .unwrap_or_else(|| "global".to_string()), + ori_text_key: ori_lang.map(|s| format!("text_{}", s)), } } @@ -977,6 +1064,13 @@ impl<'a> ImportMes<'a> { text = text.replace(key, value); } } + if self.text_key == "text" { + if let Some(ori_key) = &self.ori_text_key { + let ori_text = + obj["text"].as_str().unwrap_or("").to_string(); + obj[ori_key].set_string(ori_text); + } + } obj[&self.text_key].set_string(text.replace("\n", "\\n")); continue; } else { @@ -991,6 +1085,11 @@ impl<'a> ImportMes<'a> { text = text.replace(key, value); } } + if let Some(ori_key) = &self.ori_text_key { + let ori_text = + obj["text"].as_str().unwrap_or("").to_string(); + obj[ori_key].set_string(ori_text); + } obj[&self.text_key].set_string(text.replace("\n", "\\n")); } else { warn_not_found(text.to_string()); @@ -1015,6 +1114,15 @@ impl<'a> ImportMes<'a> { text = text.replace(key, value); } } + if self.text_key == "text" { + if let Some(ori_key) = &self.ori_text_key { + let len = list.len(); + let ori_text = + list[i].as_str().unwrap_or("").to_string(); + list[len].set_str(ori_key); + list[len + 1].set_string(ori_text); + } + } list[i].set_string(text.replace("\n", "\\n")); return; } else { diff --git a/src/types.rs b/src/types.rs index e12caaa..daa28c9 100644 --- a/src/types.rs +++ b/src/types.rs @@ -503,6 +503,9 @@ pub struct ExtraConfig { /// Workers count for packing file in Kirikiri XP3 archive in parallel. Default is 1. /// This not works when segment is disabled. pub xp3_pack_workers: usize, + #[cfg(feature = "kirikiri")] + /// Insert new language at the specified index in Kirikiri SCN script. If index is out of bounds, this flags will be ignored. + pub kirikiri_language_insert: bool, } #[derive(Clone, Copy, Debug, ValueEnum, PartialEq, Eq, PartialOrd, Ord)]