mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-09 06:18:46 +08:00
948 lines
42 KiB
Rust
948 lines
42 KiB
Rust
//! Kirikiri Scene File (.scn)
|
|
use super::mdf::Mdf;
|
|
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 fancy_regex::Regex;
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::io::{Read, Seek};
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
#[derive(Debug)]
|
|
/// Kirikiri Scene Script Builder
|
|
pub struct ScnScriptBuilder {}
|
|
|
|
impl ScnScriptBuilder {
|
|
/// Creates a new instance of `ScnScriptBuilder`
|
|
pub fn new() -> Self {
|
|
Self {}
|
|
}
|
|
}
|
|
|
|
impl ScriptBuilder for ScnScriptBuilder {
|
|
fn default_encoding(&self) -> Encoding {
|
|
Encoding::Utf8
|
|
}
|
|
|
|
fn build_script(
|
|
&self,
|
|
buf: Vec<u8>,
|
|
filename: &str,
|
|
_encoding: Encoding,
|
|
_archive_encoding: Encoding,
|
|
config: &ExtraConfig,
|
|
_archive: Option<&Box<dyn Script>>,
|
|
) -> Result<Box<dyn Script>> {
|
|
Ok(Box::new(ScnScript::new(
|
|
MemReader::new(buf),
|
|
filename,
|
|
config,
|
|
)?))
|
|
}
|
|
|
|
fn build_script_from_file(
|
|
&self,
|
|
filename: &str,
|
|
_encoding: Encoding,
|
|
_archive_encoding: Encoding,
|
|
config: &ExtraConfig,
|
|
_archive: Option<&Box<dyn Script>>,
|
|
) -> Result<Box<dyn Script>> {
|
|
if filename == "-" {
|
|
let data = crate::utils::files::read_file(filename)?;
|
|
Ok(Box::new(ScnScript::new(
|
|
MemReader::new(data),
|
|
filename,
|
|
config,
|
|
)?))
|
|
} else {
|
|
let f = std::fs::File::open(filename)?;
|
|
let reader = std::io::BufReader::new(f);
|
|
Ok(Box::new(ScnScript::new(reader, filename, config)?))
|
|
}
|
|
}
|
|
|
|
fn build_script_from_reader(
|
|
&self,
|
|
reader: Box<dyn ReadSeek>,
|
|
filename: &str,
|
|
_encoding: Encoding,
|
|
_archive_encoding: Encoding,
|
|
config: &ExtraConfig,
|
|
_archive: Option<&Box<dyn Script>>,
|
|
) -> Result<Box<dyn Script>> {
|
|
Ok(Box::new(ScnScript::new(reader, filename, config)?))
|
|
}
|
|
|
|
fn extensions(&self) -> &'static [&'static str] {
|
|
&["scn"]
|
|
}
|
|
|
|
fn script_type(&self) -> &'static ScriptType {
|
|
&ScriptType::KirikiriScn
|
|
}
|
|
|
|
fn is_this_format(&self, filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
|
|
if Path::new(filename)
|
|
.file_name()
|
|
.map(|name| {
|
|
name.to_ascii_lowercase()
|
|
.to_string_lossy()
|
|
.ends_with(".scn")
|
|
})
|
|
.unwrap_or(false)
|
|
&& buf_len >= 4
|
|
&& buf.starts_with(b"PSB\0")
|
|
{
|
|
return Some(255);
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
/// Kirikiri Scene Script
|
|
pub struct ScnScript {
|
|
psb: VirtualPsbFixed,
|
|
language_index: usize,
|
|
languages: Option<Arc<Vec<String>>>,
|
|
export_chat: bool,
|
|
filename: String,
|
|
chat_key: Option<String>,
|
|
chat_json: Option<Arc<HashMap<String, String>>>,
|
|
custom_yaml: bool,
|
|
}
|
|
|
|
impl ScnScript {
|
|
/// Creates a new `ScnScript` from the given reader and filename
|
|
///
|
|
/// * `reader` - The reader containing the PSB or MDF data
|
|
/// * `filename` - The name of the file (used for error reporting and extension detection)
|
|
/// * `config` - Extra configuration options
|
|
pub fn new<R: Read + Seek>(
|
|
mut reader: R,
|
|
filename: &str,
|
|
config: &ExtraConfig,
|
|
) -> Result<Self> {
|
|
let mut header = [0u8; 4];
|
|
reader.read_exact(&mut header)?;
|
|
if &header == b"mdf\0" {
|
|
let mut data = Vec::new();
|
|
reader.read_to_end(&mut data)?;
|
|
let decoded = Mdf::unpack(MemReaderRef::new(&data))?;
|
|
return Self::new(MemReader::new(decoded), filename, config);
|
|
}
|
|
reader.rewind()?;
|
|
let mut psb = PsbReader::open_psb(reader)
|
|
.map_err(|e| anyhow::anyhow!("Failed to open PSB from {}: {:?}", filename, e))?;
|
|
let psb = psb
|
|
.load()
|
|
.map_err(|e| anyhow::anyhow!("Failed to load PSB from {}: {:?}", filename, e))?;
|
|
Ok(Self {
|
|
psb: psb.to_psb_fixed(),
|
|
language_index: config.kirikiri_language_index.unwrap_or(0),
|
|
languages: config.kirikiri_languages.clone(),
|
|
export_chat: config.kirikiri_export_chat,
|
|
filename: filename.to_string(),
|
|
chat_key: config.kirikiri_chat_key.clone(),
|
|
chat_json: config.kirikiri_chat_json.clone(),
|
|
custom_yaml: config.custom_yaml,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Script for ScnScript {
|
|
fn default_output_script_type(&self) -> OutputScriptType {
|
|
OutputScriptType::Json
|
|
}
|
|
|
|
fn default_format_type(&self) -> FormatOptions {
|
|
FormatOptions::None
|
|
}
|
|
|
|
fn is_output_supported(&self, _: OutputScriptType) -> bool {
|
|
true
|
|
}
|
|
|
|
fn custom_output_extension<'a>(&'a self) -> &'a str {
|
|
if self.custom_yaml { "yaml" } else { "json" }
|
|
}
|
|
|
|
fn extract_messages(&self) -> Result<Vec<Message>> {
|
|
let mut messages = Vec::new();
|
|
let root = self.psb.root();
|
|
let scenes = root
|
|
.get_value("scenes")
|
|
.ok_or(anyhow::anyhow!("scenes not found"))?;
|
|
let scenes = match scenes {
|
|
PsbValueFixed::List(list) => list,
|
|
_ => return Err(anyhow::anyhow!("scenes is not a list")),
|
|
};
|
|
let language = if self.language_index != 0 {
|
|
let index = self.language_index - 1;
|
|
if let Some(lang) = root["languages"][index].as_str() {
|
|
Some(lang.to_owned())
|
|
} else if let Some(languages) = self.languages.as_ref() {
|
|
if index < languages.len() {
|
|
eprintln!(
|
|
"WARN: Language code not found in PSB, using from config. Chat messages may not be extracted correctly."
|
|
);
|
|
crate::COUNTER.inc_warning();
|
|
Some(languages[index].to_owned())
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
if self.language_index != 0 && language.is_none() {
|
|
eprintln!(
|
|
"WARN: Language index is set but language code not found in PSB. Chat messages may not be extracted correctly."
|
|
);
|
|
crate::COUNTER.inc_warning();
|
|
}
|
|
let mut comu = if self.export_chat {
|
|
Some(ExportMes::new(
|
|
self.chat_key.clone().unwrap_or("comumode".to_string()),
|
|
language.clone(),
|
|
))
|
|
} else {
|
|
None
|
|
};
|
|
for (i, oscene) in scenes.iter().enumerate() {
|
|
let scene = match oscene {
|
|
PsbValueFixed::Object(obj) => obj,
|
|
_ => return Err(anyhow::anyhow!("scene at index {} is not an object", i)),
|
|
};
|
|
if let Some(PsbValueFixed::List(texts)) = scene.get_value("texts") {
|
|
for (j, text) in texts.iter().enumerate() {
|
|
if let PsbValueFixed::List(text) = text {
|
|
let values = text.values();
|
|
if values.len() <= 1 {
|
|
continue; // Skip if there are not enough values
|
|
}
|
|
let name = &values[0];
|
|
let name = match name {
|
|
PsbValueFixed::String(s) => Some(s),
|
|
PsbValueFixed::Null => None,
|
|
_ => return Err(anyhow::anyhow!("name is not a string or null")),
|
|
};
|
|
let mut display_name;
|
|
let mut message;
|
|
if matches!(values[1], PsbValueFixed::List(_)) {
|
|
display_name = None;
|
|
message = &values[1];
|
|
} else {
|
|
if values.len() <= 2 {
|
|
continue; // Skip if there is no message
|
|
}
|
|
display_name = match &values[1] {
|
|
PsbValueFixed::String(s) => Some(s),
|
|
PsbValueFixed::Null => None,
|
|
_ => {
|
|
return Err(anyhow::anyhow!(
|
|
"display name is not a string or null at {i},{j}"
|
|
));
|
|
}
|
|
};
|
|
message = &values[2];
|
|
}
|
|
if matches!(message, PsbValueFixed::List(_)) {
|
|
let tmp = message;
|
|
if let PsbValueFixed::List(list) = tmp {
|
|
if list.len() > self.language_index {
|
|
if let PsbValueFixed::List(data) =
|
|
&list.values()[self.language_index]
|
|
{
|
|
if data.len() >= 2 {
|
|
let data = data.values();
|
|
display_name = match &data[0] {
|
|
PsbValueFixed::String(s) => Some(s),
|
|
PsbValueFixed::Null => None,
|
|
_ => {
|
|
return Err(anyhow::anyhow!(
|
|
"display name is not a string or null at {i},{j}"
|
|
));
|
|
}
|
|
};
|
|
message = &data[1];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let PsbValueFixed::String(message) = message {
|
|
match name {
|
|
Some(name) => {
|
|
let name = match display_name {
|
|
Some(name) => name.string(),
|
|
None => name.string(),
|
|
};
|
|
let message = message.string();
|
|
messages.push(Message {
|
|
name: Some(name.to_string()),
|
|
message: message.replace("\\n", "\n"),
|
|
});
|
|
}
|
|
None => {
|
|
let message = message.string();
|
|
messages.push(Message {
|
|
name: None,
|
|
message: message.replace("\\n", "\n"),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(PsbValueFixed::List(selects)) = scene.get_value("selects") {
|
|
for select in selects.iter() {
|
|
if let PsbValueFixed::Object(select) = select {
|
|
let mut text = None;
|
|
if let Some(PsbValueFixed::List(language)) = select.get_value("language") {
|
|
if language.len() > self.language_index {
|
|
let v = &language.values()[self.language_index];
|
|
if let PsbValueFixed::Object(v) = v {
|
|
text = match v.get_value("text") {
|
|
Some(PsbValueFixed::String(s)) => Some(s),
|
|
Some(PsbValueFixed::Null) => None,
|
|
None => None,
|
|
_ => {
|
|
return Err(anyhow::anyhow!(
|
|
"select text is not a string or null"
|
|
));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if text.is_none() {
|
|
text = match select.get_value("text") {
|
|
Some(PsbValueFixed::String(s)) => Some(s),
|
|
Some(PsbValueFixed::Null) => None,
|
|
None => None,
|
|
_ => {
|
|
return Err(anyhow::anyhow!(
|
|
"select text is not a string or null"
|
|
));
|
|
}
|
|
};
|
|
}
|
|
if let Some(text) = text {
|
|
let text = text.string();
|
|
messages.push(Message {
|
|
name: None,
|
|
message: text.replace("\\n", "\n"),
|
|
});
|
|
}
|
|
}
|
|
}
|
|
}
|
|
comu.as_mut().map(|c| c.export(&oscene));
|
|
}
|
|
if let Some(comu) = comu {
|
|
if !comu.messages.is_empty() {
|
|
let mut pb = std::path::PathBuf::from(&self.filename);
|
|
let filename = pb
|
|
.file_stem()
|
|
.map(|s| s.to_string_lossy())
|
|
.unwrap_or(std::borrow::Cow::from(comu.key.as_str()));
|
|
pb.set_file_name(format!("{}_{}.json", filename, comu.key));
|
|
match std::fs::File::create(&pb) {
|
|
Ok(mut f) => {
|
|
let messages: Vec<String> = comu.messages.into_iter().collect();
|
|
if let Err(e) = serde_json::to_writer_pretty(&mut f, &messages) {
|
|
eprintln!("Failed to write chat messages to {}: {:?}", pb.display(), e);
|
|
crate::COUNTER.inc_warning();
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!(
|
|
"Failed to create chat messages file {}: {:?}",
|
|
pb.display(),
|
|
e
|
|
);
|
|
crate::COUNTER.inc_warning();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
Ok(messages)
|
|
}
|
|
|
|
fn import_messages<'a>(
|
|
&'a self,
|
|
messages: Vec<Message>,
|
|
file: Box<dyn WriteSeek + 'a>,
|
|
_filename: &str,
|
|
_encoding: Encoding,
|
|
replacement: Option<&'a ReplacementTable>,
|
|
) -> Result<()> {
|
|
let mut mes = messages.iter();
|
|
let mut cur_mes = mes.next();
|
|
let mut psb = self.psb.clone();
|
|
let root = psb.root_mut();
|
|
if let Some(lang) = &self.languages {
|
|
let lang = (**lang).clone();
|
|
root["languages"] = PsbValueFixed::List(PsbListFixed {
|
|
values: lang
|
|
.into_iter()
|
|
.map(|s| PsbValueFixed::String(s.into()))
|
|
.collect(),
|
|
});
|
|
}
|
|
let language = if self.language_index != 0 {
|
|
let index = self.language_index - 1;
|
|
if let Some(lang) = root["languages"][index].as_str() {
|
|
Some(lang.to_owned())
|
|
} else {
|
|
eprintln!(
|
|
"WARN: language code not found in PSB. Some functions may not work correctly. Use --kirikiri-languages to specify language codes."
|
|
);
|
|
crate::COUNTER.inc_warning();
|
|
None
|
|
}
|
|
} else {
|
|
None
|
|
};
|
|
let scenes = &mut root["scenes"];
|
|
if !scenes.is_list() {
|
|
return Err(anyhow::anyhow!("scenes is not an array"));
|
|
}
|
|
let comu = self.chat_json.as_ref().map(|json| {
|
|
ImportMes::new(
|
|
json,
|
|
replacement,
|
|
self.chat_key.clone().unwrap_or("comumode".to_string()),
|
|
language.clone(),
|
|
)
|
|
});
|
|
for (i, scene) in scenes.members_mut().enumerate() {
|
|
if !scene.is_object() {
|
|
return Err(anyhow::anyhow!("scene at {} is not an object", i));
|
|
}
|
|
if scene["texts"].is_list() {
|
|
for (j, text) in scene["texts"].members_mut().enumerate() {
|
|
if text.is_list() {
|
|
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_string_or_null() {
|
|
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_list() {
|
|
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
|
|
{
|
|
if !text[1][self.language_index][0].is_string_or_null() {
|
|
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. (text {j} at scene {i})"
|
|
));
|
|
}
|
|
};
|
|
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[1][self.language_index][0].set_string(name);
|
|
} else {
|
|
return Err(anyhow::anyhow!(
|
|
"Name is missing for message. (text {j} at scene {i})"
|
|
));
|
|
}
|
|
}
|
|
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]
|
|
.set_string(message.replace("\n", "\\n"));
|
|
// Modify save message if exists
|
|
if text[1][self.language_index][3].is_string() {
|
|
text[1][self.language_index][3]
|
|
.set_string(get_save_message(&message, true));
|
|
}
|
|
if text[1][self.language_index][4].is_string() {
|
|
text[1][self.language_index][4]
|
|
.set_string(get_save_message(&message, false));
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
if text.len() <= 2 {
|
|
continue; // Skip if there is no message
|
|
}
|
|
if !text[1].is_string_or_null() {
|
|
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.(text {j} at scene {i})"
|
|
));
|
|
}
|
|
};
|
|
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].set_string(name);
|
|
} else {
|
|
text[0].set_string(name);
|
|
}
|
|
} else {
|
|
return Err(anyhow::anyhow!(
|
|
"Name is missing for message.(text {j} at scene {i})"
|
|
));
|
|
}
|
|
}
|
|
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].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 text[2][self.language_index].is_list()
|
|
&& text[2][self.language_index].len() >= 2
|
|
{
|
|
if !text[2][self.language_index][0].is_string_or_null() {
|
|
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.(text {j} at scene {i})"
|
|
));
|
|
}
|
|
};
|
|
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[2][self.language_index][0].set_string(name);
|
|
} else {
|
|
return Err(anyhow::anyhow!(
|
|
"Name is missing for message.(text {j} at scene {i})"
|
|
));
|
|
}
|
|
}
|
|
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]
|
|
.set_string(message.replace("\n", "\\n"));
|
|
// Modify save message if exists
|
|
if text[2][self.language_index][3].is_string() {
|
|
text[2][self.language_index][3]
|
|
.set_string(get_save_message(&message, true));
|
|
}
|
|
if text[2][self.language_index][4].is_string() {
|
|
text[2][self.language_index][4]
|
|
.set_string(get_save_message(&message, false));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if scene["selects"].is_list() {
|
|
for select in scene["selects"].members_mut() {
|
|
if select.is_object() {
|
|
if cur_mes.is_none() {
|
|
cur_mes = mes.next();
|
|
}
|
|
if select["language"].is_list()
|
|
&& {
|
|
while select["language"].len() <= self.language_index {
|
|
select["language"][self.language_index] =
|
|
select["language"][0].clone();
|
|
}
|
|
true
|
|
}
|
|
&& select["language"][self.language_index].is_object()
|
|
{
|
|
let lang_obj = &mut select["language"][self.language_index];
|
|
if lang_obj["text"].is_string() {
|
|
let m = match cur_mes.take() {
|
|
Some(m) => m,
|
|
None => {
|
|
return Err(anyhow::anyhow!("No enough messages."));
|
|
}
|
|
};
|
|
let mut text = m.message.clone();
|
|
if let Some(replacement) = replacement {
|
|
for (key, value) in replacement.map.iter() {
|
|
text = text.replace(key, value);
|
|
}
|
|
}
|
|
lang_obj["text"].set_string(text.replace("\n", "\\n"));
|
|
continue;
|
|
}
|
|
}
|
|
if select["text"].is_string() {
|
|
let m = match cur_mes.take() {
|
|
Some(m) => m,
|
|
None => {
|
|
return Err(anyhow::anyhow!("No enough messages."));
|
|
}
|
|
};
|
|
let mut text = m.message.clone();
|
|
if let Some(replacement) = replacement {
|
|
for (key, value) in replacement.map.iter() {
|
|
text = text.replace(key, value);
|
|
}
|
|
}
|
|
select["text"].set_string(text.replace("\n", "\\n"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
comu.as_ref().map(|c| c.import(scene));
|
|
}
|
|
if cur_mes.is_some() || mes.next().is_some() {
|
|
return Err(anyhow::anyhow!("Some messages were not processed."));
|
|
}
|
|
let psb = psb.to_psb(true);
|
|
let writer = PsbWriter::new(psb, file);
|
|
writer.finish().map_err(|e| {
|
|
anyhow::anyhow!("Failed to write PSB to file {}: {:?}", self.filename, e)
|
|
})?;
|
|
Ok(())
|
|
}
|
|
|
|
fn custom_export(&self, filename: &Path, encoding: Encoding) -> Result<()> {
|
|
let s = if self.custom_yaml {
|
|
serde_yaml_ng::to_string(&self.psb)
|
|
.map_err(|e| anyhow::anyhow!("Failed to serialize to YAML: {}", e))?
|
|
} else {
|
|
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<dyn WriteSeek + 'a>,
|
|
_encoding: Encoding,
|
|
output_encoding: Encoding,
|
|
) -> Result<()> {
|
|
let data = crate::utils::files::read_file(custom_filename)?;
|
|
let s = decode_to_string(output_encoding, &data, true)?;
|
|
let psb = if self.custom_yaml {
|
|
let data: VirtualPsbFixedData = serde_yaml_ng::from_str(&s)
|
|
.map_err(|e| anyhow::anyhow!("Failed to deserialize YAML: {}", e))?;
|
|
let mut psb = self.psb.clone();
|
|
psb.set_data(data);
|
|
psb.to_psb(true)
|
|
} else {
|
|
let json = json::parse(&s)?;
|
|
let mut psb = self.psb.clone();
|
|
psb.from_json(&json)?;
|
|
psb.to_psb(true)
|
|
};
|
|
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)]
|
|
struct ExportMes {
|
|
pub messages: HashSet<String>,
|
|
pub key: String,
|
|
text_key: String,
|
|
}
|
|
|
|
impl ExportMes {
|
|
pub fn new(key: String, language: Option<String>) -> Self {
|
|
Self {
|
|
messages: HashSet::new(),
|
|
key: key,
|
|
text_key: language.map_or_else(|| String::from("text"), |s| format!("text_{}", s)),
|
|
}
|
|
}
|
|
|
|
pub fn export(&mut self, value: &PsbValueFixed) {
|
|
match value {
|
|
PsbValueFixed::Object(obj) => {
|
|
for (k, v) in obj.iter() {
|
|
if k == &self.key {
|
|
if let PsbValueFixed::List(list) = v {
|
|
for item in list.iter() {
|
|
if let PsbValueFixed::Object(obj) = item {
|
|
if let Some(s) = obj[&self.text_key].as_str() {
|
|
self.messages.insert(s.replace("\\n", "\n"));
|
|
} else if let Some(s) = obj["text"].as_str() {
|
|
self.messages.insert(s.replace("\\n", "\n"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
self.export(v);
|
|
}
|
|
}
|
|
}
|
|
PsbValueFixed::List(list) => {
|
|
let list = list.values();
|
|
if list.len() > 1 {
|
|
if let PsbValueFixed::String(s) = &list[0] {
|
|
if s.string() == &self.key {
|
|
for i in 1..list.len() {
|
|
if let PsbValueFixed::String(s) = &list[i - 1] {
|
|
if s.string() == &self.text_key {
|
|
if let PsbValueFixed::String(text) = &list[i] {
|
|
self.messages
|
|
.insert(text.string().replace("\\n", "\n"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if self.text_key == "text" {
|
|
return;
|
|
}
|
|
for i in 1..list.len() {
|
|
if let PsbValueFixed::String(s) = &list[i - 1] {
|
|
if s.string() == "text" {
|
|
if let PsbValueFixed::String(text) = &list[i] {
|
|
self.messages
|
|
.insert(text.string().replace("\\n", "\n"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
for item in list {
|
|
self.export(item);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct ImportMes<'a> {
|
|
messages: &'a Arc<HashMap<String, String>>,
|
|
replacement: Option<&'a ReplacementTable>,
|
|
key: String,
|
|
text_key: String,
|
|
}
|
|
|
|
impl<'a> ImportMes<'a> {
|
|
pub fn new(
|
|
messages: &'a Arc<HashMap<String, String>>,
|
|
replacement: Option<&'a ReplacementTable>,
|
|
key: String,
|
|
lang: Option<String>,
|
|
) -> Self {
|
|
Self {
|
|
messages,
|
|
replacement,
|
|
key: key,
|
|
text_key: lang.map_or_else(|| String::from("text"), |s| format!("text_{}", s)),
|
|
}
|
|
}
|
|
|
|
pub fn import(&self, value: &mut PsbValueFixed) {
|
|
match value {
|
|
PsbValueFixed::Object(obj) => {
|
|
for (k, v) in obj.iter_mut() {
|
|
if k == &self.key {
|
|
for obj in v.members_mut() {
|
|
if let Some(text) = obj[&self.text_key].as_str() {
|
|
if let Some(replace_text) = self.messages.get(text) {
|
|
let mut text = replace_text.clone();
|
|
if let Some(replacement) = self.replacement {
|
|
for (key, value) in replacement.map.iter() {
|
|
text = text.replace(key, value);
|
|
}
|
|
}
|
|
obj[&self.text_key].set_string(text.replace("\n", "\\n"));
|
|
return;
|
|
} else {
|
|
eprintln!(
|
|
"Warning: chat message '{}' not found in translation table.",
|
|
text
|
|
);
|
|
crate::COUNTER.inc_warning();
|
|
}
|
|
}
|
|
if let Some(text) = obj["text"].as_str() {
|
|
if let Some(replace_text) = self.messages.get(text) {
|
|
let mut text = replace_text.clone();
|
|
if let Some(replacement) = self.replacement {
|
|
for (key, value) in replacement.map.iter() {
|
|
text = text.replace(key, value);
|
|
}
|
|
}
|
|
obj[&self.text_key].set_string(text.replace("\n", "\\n"));
|
|
} else {
|
|
eprintln!(
|
|
"Warning: chat message '{}' not found in translation table.",
|
|
text
|
|
);
|
|
crate::COUNTER.inc_warning();
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
self.import(v);
|
|
}
|
|
}
|
|
}
|
|
PsbValueFixed::List(list) => {
|
|
if list.len() > 1 {
|
|
if list[0] == self.key {
|
|
for i in 1..list.len() {
|
|
if list[i - 1] == self.text_key {
|
|
if let Some(text) = list[i].as_str() {
|
|
if let Some(replace_text) = self.messages.get(text) {
|
|
let mut text = replace_text.clone();
|
|
if let Some(replacement) = self.replacement {
|
|
for (key, value) in replacement.map.iter() {
|
|
text = text.replace(key, value);
|
|
}
|
|
}
|
|
list[i].set_string(text.replace("\n", "\\n"));
|
|
return;
|
|
} else {
|
|
eprintln!(
|
|
"Warning: chat message '{}' not found in translation table.",
|
|
text
|
|
);
|
|
crate::COUNTER.inc_warning();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if self.text_key == "text" {
|
|
return;
|
|
}
|
|
for i in 1..list.len() {
|
|
if list[i - 1] == "text" {
|
|
if let Some(text) = list[i].as_str() {
|
|
if let Some(replace_text) = self.messages.get(text) {
|
|
let mut text = replace_text.clone();
|
|
if let Some(replacement) = self.replacement {
|
|
for (key, value) in replacement.map.iter() {
|
|
text = text.replace(key, value);
|
|
}
|
|
}
|
|
let len = list.len();
|
|
list[len].set_str(&self.text_key);
|
|
list[len + 1].set_string(text.replace("\n", "\\n"));
|
|
return;
|
|
} else {
|
|
eprintln!(
|
|
"Warning: chat message '{}' not found in translation table.",
|
|
text
|
|
);
|
|
crate::COUNTER.inc_warning();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
for item in list.iter_mut() {
|
|
self.import(item);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
lazy_static::lazy_static! {
|
|
static ref CONTROL: Regex = Regex::new("%[^;]*;").unwrap();
|
|
static ref RUBY: Regex = Regex::new(r"\[([^\]]*)\](.?)").unwrap();
|
|
}
|
|
|
|
fn get_save_message(s: &str, in_ruby: bool) -> String {
|
|
let mut s = s.replace("\n", "");
|
|
s = CONTROL.replace_all(&s, "").to_string();
|
|
s = RUBY
|
|
.replace_all(&s, if in_ruby { "$1" } else { "$2" })
|
|
.to_string();
|
|
s
|
|
}
|
|
|
|
#[test]
|
|
fn test_get_save_message() {
|
|
let s = "%n;Test\n[ruby]测[test\\]试%ok;[ok]";
|
|
assert_eq!(get_save_message(s, true), "Testrubytest\\ok");
|
|
assert_eq!(get_save_message(s, false), "Test测试");
|
|
}
|