mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-10 00:05:07 +08:00
711 lines
31 KiB
Rust
711 lines
31 KiB
Rust
use crate::ext::io::*;
|
|
use crate::ext::psb::*;
|
|
use crate::scripts::base::*;
|
|
use crate::types::*;
|
|
use anyhow::Result;
|
|
use emote_psb::{PsbReader, PsbWriter};
|
|
use std::collections::{HashMap, HashSet};
|
|
use std::io::{Read, Seek};
|
|
use std::path::Path;
|
|
use std::sync::Arc;
|
|
|
|
#[derive(Debug)]
|
|
pub struct ScnScriptBuilder {}
|
|
|
|
impl 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,
|
|
) -> 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,
|
|
) -> 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,
|
|
) -> 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(".ks.scn")
|
|
})
|
|
.unwrap_or(false)
|
|
&& buf_len >= 4
|
|
&& buf.starts_with(b"PSB\0")
|
|
{
|
|
return Some(255);
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct ScnScript {
|
|
psb: VirtualPsbFixed,
|
|
language_index: usize,
|
|
export_comumode: bool,
|
|
filename: String,
|
|
comumode_json: Option<Arc<HashMap<String, String>>>,
|
|
}
|
|
|
|
impl ScnScript {
|
|
pub fn new<R: Read + Seek>(reader: R, filename: &str, config: &ExtraConfig) -> Result<Self> {
|
|
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),
|
|
export_comumode: config.kirikiri_export_comumode,
|
|
filename: filename.to_string(),
|
|
comumode_json: config.kirikiri_comumode_json.clone(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl Script for ScnScript {
|
|
fn default_output_script_type(&self) -> OutputScriptType {
|
|
OutputScriptType::Json
|
|
}
|
|
|
|
fn default_format_type(&self) -> FormatOptions {
|
|
FormatOptions::None
|
|
}
|
|
|
|
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 mut comu = if self.export_comumode {
|
|
Some(ExportComuMes::new())
|
|
} 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("comumode"));
|
|
pb.set_file_name(format!("{}_comumode.json", filename));
|
|
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 COMU messages to {}: {:?}", pb.display(), e);
|
|
crate::COUNTER.inc_warning();
|
|
}
|
|
}
|
|
Err(e) => {
|
|
eprintln!(
|
|
"Failed to create COMU 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>,
|
|
_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();
|
|
let scenes = &mut root["scenes"];
|
|
if !scenes.is_list() {
|
|
return Err(anyhow::anyhow!("scenes is not an array"));
|
|
}
|
|
let comu = self
|
|
.comumode_json
|
|
.as_ref()
|
|
.map(|json| ImportComuMes::new(json, replacement));
|
|
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_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() {
|
|
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].set_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].set_string(message.replace("\n", "\\n"));
|
|
} else if text[1].is_list() {
|
|
if text[1].len() > self.language_index
|
|
&& 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."));
|
|
}
|
|
};
|
|
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].set_string(name);
|
|
} else {
|
|
text[0].set_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]
|
|
.set_string(message.replace("\n", "\\n"));
|
|
}
|
|
}
|
|
}
|
|
} 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."));
|
|
}
|
|
};
|
|
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."));
|
|
}
|
|
}
|
|
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() {
|
|
if text[2].len() > self.language_index
|
|
&& 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."));
|
|
}
|
|
};
|
|
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].set_string(name);
|
|
} else {
|
|
text[0].set_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]
|
|
.set_string(message.replace("\n", "\\n"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
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()
|
|
&& select["language"].len() > self.language_index
|
|
&& 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();
|
|
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 ExportComuMes {
|
|
pub messages: HashSet<String>,
|
|
}
|
|
|
|
impl ExportComuMes {
|
|
pub fn new() -> Self {
|
|
Self {
|
|
messages: HashSet::new(),
|
|
}
|
|
}
|
|
|
|
pub fn export(&mut self, value: &PsbValueFixed) {
|
|
match value {
|
|
PsbValueFixed::Object(obj) => {
|
|
for (k, v) in obj.iter() {
|
|
if k == "comumode" {
|
|
if let PsbValueFixed::List(list) = v {
|
|
for item in list.iter() {
|
|
if let PsbValueFixed::Object(obj) = item {
|
|
if let Some(PsbValueFixed::String(s)) = obj.get_value("text") {
|
|
self.messages.insert(s.string().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() == "comumode" {
|
|
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 ImportComuMes<'a> {
|
|
messages: &'a Arc<HashMap<String, String>>,
|
|
replacement: Option<&'a ReplacementTable>,
|
|
}
|
|
|
|
impl<'a> ImportComuMes<'a> {
|
|
pub fn new(
|
|
messages: &'a Arc<HashMap<String, String>>,
|
|
replacement: Option<&'a ReplacementTable>,
|
|
) -> Self {
|
|
Self {
|
|
messages,
|
|
replacement,
|
|
}
|
|
}
|
|
|
|
pub fn import(&self, value: &mut PsbValueFixed) {
|
|
match value {
|
|
PsbValueFixed::Object(obj) => {
|
|
for (k, v) in obj.iter_mut() {
|
|
if k == "comumode" {
|
|
for obj in v.members_mut() {
|
|
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["text"].set_string(text.replace("\n", "\\n"));
|
|
} else {
|
|
eprintln!(
|
|
"Warning: COMU 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] == "comumode" {
|
|
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);
|
|
}
|
|
}
|
|
list[i].set_string(text.replace("\n", "\\n"));
|
|
} else {
|
|
eprintln!(
|
|
"Warning: COMU message '{}' not found in translation table.",
|
|
text
|
|
);
|
|
crate::COUNTER.inc_warning();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
for item in list.iter_mut() {
|
|
self.import(item);
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|