From b1aae1c75deda8eb0be725665ec33049f33a0dd6 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Fri, 19 Sep 2025 21:42:04 +0800 Subject: [PATCH] Add multiple part messages support --- src/args.rs | 3 + src/main.rs | 672 ++++++++++++++++++++++++++++++ src/scripts/base.rs | 57 +++ src/scripts/softpal/scr/disasm.rs | 66 +++ src/scripts/softpal/scr/mod.rs | 205 +++++++++ src/types.rs | 1 + 6 files changed, 1004 insertions(+) diff --git a/src/args.rs b/src/args.rs index 8529c28..35d4ace 100644 --- a/src/args.rs +++ b/src/args.rs @@ -473,6 +473,9 @@ pub struct Arg { /// Whether to use compression for Softpal Pgd images. /// WARN: Compress may cause image broken. pub pgd_compress: bool, + #[arg(long, global = true)] + /// Disable multiple messages section support. + pub no_multi_message: bool, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/main.rs b/src/main.rs index a8d2555..5f38fc8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -571,6 +571,166 @@ pub fn export_script( if !script_file.is_output_supported(of) { of = script_file.default_output_script_type(); } + if !arg.no_multi_message && !of.is_custom() && script_file.multiple_message_files() + { + let mmes = script_file.extract_multiple_messages()?; + if mmes.is_empty() { + eprintln!("No messages found in {}", f.name()); + COUNTER.inc(types::ScriptResult::Ignored); + continue; + } + let ext = of.as_ref(); + let mut out_dir = std::path::PathBuf::from(&odir).join(f.name()); + if arg.output_no_extra_ext { + out_dir.remove_all_extensions(); + } else { + out_dir.set_extension(""); + } + std::fs::create_dir_all(&out_dir)?; + for (name, data) in mmes { + let ofp = out_dir.join(name).with_extension(ext); + match of { + types::OutputScriptType::Json => { + let enc = get_output_encoding(arg); + let s = match serde_json::to_string_pretty(&data) { + Ok(s) => s, + Err(e) => { + eprintln!("Error serializing messages to JSON: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let b = match utils::encoding::encode_string(enc, &s, false) { + Ok(b) => b, + Err(e) => { + eprintln!("Error encoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let mut f = match utils::files::write_file(&ofp) { + Ok(f) => f, + Err(e) => { + eprintln!("Error writing file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + }; + match f.write_all(&b) { + Ok(_) => {} + Err(e) => { + eprintln!("Error writing to file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::M3t + | types::OutputScriptType::M3ta + | types::OutputScriptType::M3tTxt => { + let enc = get_output_encoding(arg); + let s = output_scripts::m3t::M3tDumper::dump(&data); + let b = match utils::encoding::encode_string(enc, &s, false) { + Ok(b) => b, + Err(e) => { + eprintln!("Error encoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let mut f = match utils::files::write_file(&ofp) { + Ok(f) => f, + Err(e) => { + eprintln!("Error writing file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + }; + match f.write_all(&b) { + Ok(_) => {} + Err(e) => { + eprintln!("Error writing to file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::Yaml => { + let enc = get_output_encoding(arg); + let s = match serde_yaml_ng::to_string(&data) { + Ok(s) => s, + Err(e) => { + eprintln!("Error serializing messages to YAML: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let b = match utils::encoding::encode_string(enc, &s, false) { + Ok(b) => b, + Err(e) => { + eprintln!("Error encoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let mut f = match utils::files::write_file(&ofp) { + Ok(f) => f, + Err(e) => { + eprintln!("Error writing file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + }; + match f.write_all(&b) { + Ok(_) => {} + Err(e) => { + eprintln!("Error writing to file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::Pot | types::OutputScriptType::Po => { + let enc = get_output_encoding(arg); + let s = match output_scripts::po::PoDumper::new().dump(&data, enc) { + Ok(s) => s, + Err(e) => { + eprintln!("Error dumping messages to PO format: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let b = match utils::encoding::encode_string(enc, &s, false) { + Ok(b) => b, + Err(e) => { + eprintln!("Error encoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let mut f = match utils::files::write_file(&ofp) { + Ok(f) => f, + Err(e) => { + eprintln!("Error writing file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + }; + match f.write_all(&b) { + Ok(_) => {} + Err(e) => { + eprintln!("Error writing to file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::Custom => {} + } + } + COUNTER.inc(types::ScriptResult::Ok); + continue; + } let mes = if of.is_custom() { Vec::new() } else { @@ -981,6 +1141,188 @@ pub fn export_script( if !script.is_output_supported(of) { of = script.default_output_script_type(); } + if !arg.no_multi_message && !of.is_custom() && script.multiple_message_files() { + let mmes = script.extract_multiple_messages()?; + if mmes.is_empty() { + eprintln!("No messages found"); + return Ok(types::ScriptResult::Ignored); + } + let ext = of.as_ref(); + let out_dir = if let Some(output) = output.as_ref() { + if let Some(root_dir) = root_dir { + let f = std::path::PathBuf::from(filename); + let mut pb = std::path::PathBuf::from(output); + let rpath = utils::files::relative_path(root_dir, &f); + if let Some(parent) = rpath.parent() { + pb.push(parent); + } + if let Some(fname) = f.file_name() { + pb.push(fname); + } + if arg.output_no_extra_ext { + pb.remove_all_extensions(); + } else { + pb.set_extension(""); + } + pb.to_string_lossy().into_owned() + } else { + output.clone() + } + } else { + let mut pb = std::path::PathBuf::from(filename); + if arg.output_no_extra_ext { + pb.remove_all_extensions(); + } else { + pb.set_extension(""); + } + pb.to_string_lossy().into_owned() + }; + std::fs::create_dir_all(&out_dir)?; + let outdir = std::path::PathBuf::from(&out_dir); + for (name, data) in mmes { + let ofp = outdir.join(name).with_extension(ext); + match of { + types::OutputScriptType::Json => { + let enc = get_output_encoding(arg); + let s = match serde_json::to_string_pretty(&data) { + Ok(s) => s, + Err(e) => { + eprintln!("Error serializing messages to JSON: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let b = match utils::encoding::encode_string(enc, &s, false) { + Ok(b) => b, + Err(e) => { + eprintln!("Error encoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let mut f = match utils::files::write_file(&ofp) { + Ok(f) => f, + Err(e) => { + eprintln!("Error writing file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + }; + match f.write_all(&b) { + Ok(_) => {} + Err(e) => { + eprintln!("Error writing to file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::M3t + | types::OutputScriptType::M3ta + | types::OutputScriptType::M3tTxt => { + let enc = get_output_encoding(arg); + let s = output_scripts::m3t::M3tDumper::dump(&data); + let b = match utils::encoding::encode_string(enc, &s, false) { + Ok(b) => b, + Err(e) => { + eprintln!("Error encoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let mut f = match utils::files::write_file(&ofp) { + Ok(f) => f, + Err(e) => { + eprintln!("Error writing file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + }; + match f.write_all(&b) { + Ok(_) => {} + Err(e) => { + eprintln!("Error writing to file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::Yaml => { + let enc = get_output_encoding(arg); + let s = match serde_yaml_ng::to_string(&data) { + Ok(s) => s, + Err(e) => { + eprintln!("Error serializing messages to YAML: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let b = match utils::encoding::encode_string(enc, &s, false) { + Ok(b) => b, + Err(e) => { + eprintln!("Error encoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let mut f = match utils::files::write_file(&ofp) { + Ok(f) => f, + Err(e) => { + eprintln!("Error writing file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + }; + match f.write_all(&b) { + Ok(_) => {} + Err(e) => { + eprintln!("Error writing to file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::Pot | types::OutputScriptType::Po => { + let enc = get_output_encoding(arg); + let s = match output_scripts::po::PoDumper::new().dump(&data, enc) { + Ok(s) => s, + Err(e) => { + eprintln!("Error dumping messages to PO format: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let b = match utils::encoding::encode_string(enc, &s, false) { + Ok(b) => b, + Err(e) => { + eprintln!("Error encoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let mut f = match utils::files::write_file(&ofp) { + Ok(f) => f, + Err(e) => { + eprintln!("Error writing file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + }; + match f.write_all(&b) { + Ok(_) => {} + Err(e) => { + eprintln!("Error writing to file {}: {}", ofp.display(), e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::Custom => {} + } + COUNTER.inc(types::ScriptResult::Ok); + } + return Ok(types::ScriptResult::Ok); + } let mes = if of.is_custom() { Vec::new() } else { @@ -1165,6 +1507,215 @@ pub fn import_script( if !script_file.is_output_supported(of) { of = script_file.default_output_script_type(); } + if !arg.no_multi_message && !of.is_custom() && script_file.multiple_message_files() + { + let out_dir = std::path::PathBuf::from(&odir) + .join(f.name()) + .with_extension(""); + let outfiles = utils::files::find_ext_files( + &out_dir.to_string_lossy(), + false, + &[of.as_ref()], + )?; + if outfiles.is_empty() { + if imp_cfg.warn_when_output_file_not_found { + eprintln!( + "Warning: No output files found in {}, using file from original archive.", + out_dir.display() + ); + COUNTER.inc_warning(); + } else { + COUNTER.inc(types::ScriptResult::Ignored); + } + continue; + } + let fmt = match imp_cfg.patched_format { + Some(fmt) => match fmt { + types::FormatType::Fixed => types::FormatOptions::Fixed { + length: imp_cfg.patched_fixed_length.unwrap_or(32), + keep_original: imp_cfg.patched_keep_original, + break_words: imp_cfg.patched_break_words, + insert_fullwidth_space_at_line_start: imp_cfg + .patched_insert_fullwidth_space_at_line_start, + break_with_sentence: imp_cfg.patched_break_with_sentence, + #[cfg(feature = "jieba")] + break_chinese_words: !imp_cfg.patched_no_break_chinese_words, + #[cfg(feature = "jieba")] + jieba_dict: arg.jieba_dict.clone(), + }, + types::FormatType::None => types::FormatOptions::None, + }, + None => script.default_format_type(), + }; + let mut mmes = std::collections::HashMap::new(); + for out_f in outfiles { + let name = utils::files::relative_path(&out_dir, &out_f) + .with_extension("") + .to_string_lossy() + .into_owned(); + let mut mes = match of { + types::OutputScriptType::Json => { + let enc = get_output_encoding(arg); + let b = match utils::files::read_file(&out_f) { + Ok(b) => b, + Err(e) => { + eprintln!("Error reading file {}: {}", out_f, e); + COUNTER.inc_error(); + continue; + } + }; + let s = match utils::encoding::decode_to_string(enc, &b, true) { + Ok(s) => s, + Err(e) => { + eprintln!("Error decoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + match serde_json::from_str::>(&s) { + Ok(mes) => mes, + Err(e) => { + eprintln!("Error parsing JSON: {}", e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::M3t + | types::OutputScriptType::M3ta + | types::OutputScriptType::M3tTxt => { + let enc = get_output_encoding(arg); + let b = match utils::files::read_file(&out_f) { + Ok(b) => b, + Err(e) => { + eprintln!("Error reading file {}: {}", out_f, e); + COUNTER.inc_error(); + continue; + } + }; + let s = match utils::encoding::decode_to_string(enc, &b, true) { + Ok(s) => s, + Err(e) => { + eprintln!("Error decoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + let mut parser = output_scripts::m3t::M3tParser::new( + &s, + arg.llm_trans_mark.as_ref().map(|s| s.as_str()), + ); + match parser.parse() { + Ok(mes) => mes, + Err(e) => { + eprintln!("Error parsing M3T: {}", e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::Yaml => { + let enc = get_output_encoding(arg); + let b = match utils::files::read_file(&out_f) { + Ok(b) => b, + Err(e) => { + eprintln!("Error reading file {}: {}", out_f, e); + COUNTER.inc_error(); + continue; + } + }; + let s = match utils::encoding::decode_to_string(enc, &b, true) { + Ok(s) => s, + Err(e) => { + eprintln!("Error decoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + match serde_yaml_ng::from_str::>(&s) { + Ok(mes) => mes, + Err(e) => { + eprintln!("Error parsing YAML: {}", e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::Pot | types::OutputScriptType::Po => { + let enc = get_output_encoding(arg); + let b = match utils::files::read_file(&out_f) { + Ok(b) => b, + Err(e) => { + eprintln!("Error reading file {}: {}", out_f, e); + COUNTER.inc_error(); + continue; + } + }; + let s = match utils::encoding::decode_to_string(enc, &b, true) { + Ok(s) => s, + Err(e) => { + eprintln!("Error decoding string: {}", e); + COUNTER.inc_error(); + continue; + } + }; + match output_scripts::po::PoParser::new( + &s, + arg.llm_trans_mark.as_ref().map(|s| s.as_str()), + ) + .parse() + { + Ok(mes) => mes, + Err(e) => { + eprintln!("Error parsing PO: {}", e); + COUNTER.inc_error(); + continue; + } + } + } + types::OutputScriptType::Custom => Vec::new(), + }; + if mes.is_empty() { + eprintln!( + "No messages found in {}, using file from original archive.", + out_f + ); + continue; + } + match name_csv { + Some(name_table) => { + utils::name_replacement::replace_message(&mut mes, name_table); + } + None => {} + } + format::fmt_message(&mut mes, fmt.clone(), *builder.script_type())?; + mmes.insert(name, mes); + } + if mmes.is_empty() { + COUNTER.inc(types::ScriptResult::Ignored); + continue; + } + let encoding = get_patched_encoding(imp_cfg, builder); + match script_file.import_multiple_messages( + mmes, + writer, + f.name(), + encoding, + repl, + ) { + Ok(_) => {} + Err(e) => { + eprintln!("Error importing messages to script '{}': {}", filename, e); + COUNTER.inc_error(); + if arg.backtrace { + eprintln!("Backtrace: {}", e.backtrace()); + } + continue; + } + } + COUNTER.inc(types::ScriptResult::Ok); + continue; + } let mut out_path = std::path::PathBuf::from(&odir).join(f.name()); if arg.output_no_extra_ext { out_path.remove_all_extensions(); @@ -1496,6 +2047,127 @@ pub fn import_script( if !script.is_output_supported(of) { of = script.default_output_script_type(); } + if !arg.no_multi_message && !of.is_custom() && script.multiple_message_files() { + let out_dir = if let Some(root_dir) = root_dir { + let f = std::path::PathBuf::from(filename); + let mut pb = std::path::PathBuf::from(&imp_cfg.output); + let rpath = utils::files::relative_path(root_dir, &f); + if let Some(parent) = rpath.parent() { + pb.push(parent); + } + if let Some(fname) = f.file_name() { + pb.push(fname); + } + if arg.output_no_extra_ext { + pb.remove_all_extensions(); + } else { + pb.set_extension(""); + } + pb.to_string_lossy().into_owned() + } else { + imp_cfg.output.clone() + }; + let outfiles = utils::files::find_ext_files(&out_dir, false, &[of.as_ref()])?; + if outfiles.is_empty() { + eprintln!("No output files found"); + return Ok(types::ScriptResult::Ignored); + } + let fmt = match imp_cfg.patched_format { + Some(fmt) => match fmt { + types::FormatType::Fixed => types::FormatOptions::Fixed { + length: imp_cfg.patched_fixed_length.unwrap_or(32), + keep_original: imp_cfg.patched_keep_original, + break_words: imp_cfg.patched_break_words, + insert_fullwidth_space_at_line_start: imp_cfg + .patched_insert_fullwidth_space_at_line_start, + break_with_sentence: imp_cfg.patched_break_with_sentence, + #[cfg(feature = "jieba")] + break_chinese_words: !imp_cfg.patched_no_break_chinese_words, + #[cfg(feature = "jieba")] + jieba_dict: arg.jieba_dict.clone(), + }, + types::FormatType::None => types::FormatOptions::None, + }, + None => script.default_format_type(), + }; + let mut mmes = std::collections::HashMap::new(); + for out_f in outfiles { + let name = utils::files::relative_path(&out_dir, &out_f) + .with_extension("") + .to_string_lossy() + .into_owned(); + let mut mes = match of { + types::OutputScriptType::Json => { + let enc = get_output_encoding(arg); + let b = utils::files::read_file(&out_f)?; + let s = utils::encoding::decode_to_string(enc, &b, true)?; + serde_json::from_str::>(&s)? + } + types::OutputScriptType::M3t + | types::OutputScriptType::M3ta + | types::OutputScriptType::M3tTxt => { + let enc = get_output_encoding(arg); + let b = utils::files::read_file(&out_f)?; + let s = utils::encoding::decode_to_string(enc, &b, true)?; + let mut parser = output_scripts::m3t::M3tParser::new( + &s, + arg.llm_trans_mark.as_ref().map(|s| s.as_str()), + ); + parser.parse()? + } + types::OutputScriptType::Yaml => { + let enc = get_output_encoding(arg); + let b = utils::files::read_file(&out_f)?; + let s = utils::encoding::decode_to_string(enc, &b, true)?; + serde_yaml_ng::from_str::>(&s)? + } + types::OutputScriptType::Pot | types::OutputScriptType::Po => { + let enc = get_output_encoding(arg); + let b = utils::files::read_file(&out_f)?; + let s = utils::encoding::decode_to_string(enc, &b, true)?; + let mut parser = output_scripts::po::PoParser::new( + &s, + arg.llm_trans_mark.as_ref().map(|s| s.as_str()), + ); + parser.parse()? + } + types::OutputScriptType::Custom => { + Vec::new() // Custom scripts handle their own messages + } + }; + if mes.is_empty() { + eprintln!("No messages found in {}", out_f); + continue; + } + match name_csv { + Some(name_table) => { + utils::name_replacement::replace_message(&mut mes, name_table); + } + None => {} + } + format::fmt_message(&mut mes, fmt.clone(), *builder.script_type())?; + mmes.insert(name, mes); + } + let patched_f = if let Some(root_dir) = root_dir { + let f = std::path::PathBuf::from(filename); + let mut pb = std::path::PathBuf::from(&imp_cfg.patched); + let rpath = utils::files::relative_path(root_dir, &f); + if let Some(parent) = rpath.parent() { + pb.push(parent); + } + if let Some(fname) = f.file_name() { + pb.push(fname); + } + pb.set_extension(builder.extensions().first().unwrap_or(&"")); + pb.to_string_lossy().into_owned() + } else { + imp_cfg.patched.clone() + }; + utils::files::make_sure_dir_exists(&patched_f)?; + let encoding = get_patched_encoding(imp_cfg, builder); + script.import_multiple_messages_filename(mmes, &patched_f, encoding, repl)?; + return Ok(types::ScriptResult::Ok); + } let out_f = if let Some(root_dir) = root_dir { let f = std::path::PathBuf::from(filename); let mut pb = std::path::PathBuf::from(&imp_cfg.output); diff --git a/src/scripts/base.rs b/src/scripts/base.rs index 65b2288..90d7810 100644 --- a/src/scripts/base.rs +++ b/src/scripts/base.rs @@ -2,6 +2,7 @@ use crate::ext::io::*; use crate::types::*; use anyhow::Result; +use std::collections::HashMap; use std::io::{Read, Seek, Write}; /// A trait for reading and seeking in a stream. @@ -270,6 +271,11 @@ pub trait Script: std::fmt::Debug + std::any::Any { /// Returns the default format options for this script. fn default_format_type(&self) -> FormatOptions; + /// Returns true if this script can contains multiple message files. + fn multiple_message_files(&self) -> bool { + false + } + /// Extract messages from this script. fn extract_messages(&self) -> Result> { if !self.is_archive() { @@ -280,6 +286,16 @@ pub trait Script: std::fmt::Debug + std::any::Any { Ok(vec![]) } + /// Extract multiple messages from this script. + fn extract_multiple_messages(&self) -> Result>> { + if !self.multiple_message_files() { + return Err(anyhow::anyhow!( + "This script type does not support extracting multiple message files." + )); + } + Ok(HashMap::new()) + } + /// Import messages into this script. /// /// * `messages` - The messages to import. @@ -303,6 +319,29 @@ pub trait Script: std::fmt::Debug + std::any::Any { Ok(()) } + /// Import multiple messages into this script. + /// + /// * `messages` - A map of filenames to messages to import. + /// * `file` - A writer with seek capabilities to write the patched scripts. + /// * `filename` - The path of the file to write the patched scripts. + /// * `encoding` - The encoding to use for the patched scripts. + /// * `replacement` - An optional replacement table for message replacements.s + fn import_multiple_messages<'a>( + &'a self, + _messages: HashMap>, + _file: Box, + _filename: &str, + _encoding: Encoding, + _replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + if !self.multiple_message_files() { + return Err(anyhow::anyhow!( + "This script type does not support importing multiple message files." + )); + } + Ok(()) + } + /// Import messages into this script. /// /// * `messages` - The messages to import. @@ -321,6 +360,24 @@ pub trait Script: std::fmt::Debug + std::any::Any { self.import_messages(messages, Box::new(f), filename, encoding, replacement) } + /// Import multiple messages into this script. + /// + /// * `messages` - A map of filenames to messages to import. + /// * `filename` - The path of the file to write the patched scripts. + /// * `encoding` - The encoding to use for the patched scripts. + /// * `replacement` - An optional replacement table for message replacements. + fn import_multiple_messages_filename( + &self, + messages: HashMap>, + filename: &str, + encoding: Encoding, + replacement: Option<&ReplacementTable>, + ) -> Result<()> { + let f = std::fs::File::create(filename)?; + let f = std::io::BufWriter::new(f); + self.import_multiple_messages(messages, Box::new(f), filename, encoding, replacement) + } + /// Exports data from this script. /// /// * `filename` - The path of the file to write the exported data. diff --git a/src/scripts/softpal/scr/disasm.rs b/src/scripts/softpal/scr/disasm.rs index 9ec5371..9cfac19 100644 --- a/src/scripts/softpal/scr/disasm.rs +++ b/src/scripts/softpal/scr/disasm.rs @@ -315,12 +315,23 @@ pub struct Disasm<'a> { variables: HashMap, stack: Vec, strs: Vec, + pre_is_hover_text_move: bool, } #[derive(Debug)] pub enum StringType { Name, Message, + /// Hover text + Hover, + /// Label + Label, +} + +impl StringType { + pub fn is_label(&self) -> bool { + matches!(self, StringType::Label) + } } #[derive(Debug)] @@ -347,6 +358,7 @@ impl<'a> Disasm<'a> { variables: HashMap::new(), stack: Vec::new(), strs: Vec::new(), + pre_is_hover_text_move: false, }) } @@ -362,6 +374,11 @@ impl<'a> Disasm<'a> { if let Some(writer) = writer.as_mut() { self.write_instruction_to(&instr, writer)?; } + let is_hover_text_move = instr.opcode == MOV + && instr.operands[0].typ() == OperandType::Variable + && instr.operands[0].value() == 2 + && instr.operands[1].typ() == OperandType::Literal + && instr.operands[1].value() < 0xFFFFFFF; if instr.is_message() { self.handle_message_instruction()?; } else if instr.opcode == MOV { @@ -378,6 +395,7 @@ impl<'a> Disasm<'a> { self.stack.clear(); self.variables.clear(); } + self.pre_is_hover_text_move = is_hover_text_move; } Ok(self.strs) } @@ -538,6 +556,12 @@ impl<'a> Disasm<'a> { && self.variables.contains_key(&instr.operands[0].value()) { let var = self.variables.get(&instr.operands[0].value()).unwrap(); + if self.pre_is_hover_text_move && instr.operands[0].value() == 2 { + self.strs.push(PalString { + offset: var.offset, + typ: StringType::Hover, + }); + } self.stack.push(*var); } else { self.stack.push(instr.operands[0]); @@ -589,6 +613,12 @@ impl<'a> Disasm<'a> { 0x60002 => { self.handle_select_choice_instruction()?; } + 0x20014 => { + self.handle_another_message()?; + } + 0xf0002 => { + self.handle_label()?; + } _ => { self.stack.clear(); } @@ -603,6 +633,42 @@ impl<'a> Disasm<'a> { Ok(()) } + fn handle_another_message(&mut self) -> Result<()> { + if self.stack.len() < 3 { + return Ok(()); + } + let _message_id = self.stack.pop().unwrap(); + let name = self.stack.pop().unwrap(); + let message = self.stack.pop().unwrap(); + if name.typ() != OperandType::Literal || message.typ() != OperandType::Literal { + return Ok(()); + } + self.strs.push(PalString { + offset: name.offset, + typ: StringType::Name, + }); + self.strs.push(PalString { + offset: message.offset, + typ: StringType::Message, + }); + Ok(()) + } + + fn handle_label(&mut self) -> Result<()> { + if self.stack.len() < 1 { + return Ok(()); + } + let label = self.stack.pop().unwrap(); + if label.typ() != OperandType::Literal { + return Ok(()); + } + self.strs.push(PalString { + offset: label.offset, + typ: StringType::Label, + }); + Ok(()) + } + fn handle_message_instruction_internal(&mut self) -> Result<()> { if self.stack.len() < 4 { return Ok(()); diff --git a/src/scripts/softpal/scr/mod.rs b/src/scripts/softpal/scr/mod.rs index 86eedac..b1e8e6d 100644 --- a/src/scripts/softpal/scr/mod.rs +++ b/src/scripts/softpal/scr/mod.rs @@ -150,6 +150,10 @@ impl Script for SoftpalScript { true } + fn multiple_message_files(&self) -> bool { + true + } + fn extract_messages(&self) -> Result> { let mut messages = Vec::new(); let mut name = None; @@ -169,11 +173,60 @@ impl Script for SoftpalScript { name: name.take(), message: text, }), + StringType::Hover => messages.push(Message::new(text, None)), + StringType::Label => {} // Ignore labels } } Ok(messages) } + fn extract_multiple_messages(&self) -> Result>> { + let mut hovers = Vec::new(); + let mut messages = Vec::new(); + let mut label = None; + let mut name = None; + let mut result = HashMap::new(); + for str in &self.strs { + let addr = self.data.cpeek_u32_at(str.offset as u64)?; + let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?; + let text = + decode_to_string(self.encoding, text.as_bytes(), false)?.replace("
", "\n"); + match str.typ { + StringType::Name => { + if text.is_empty() { + continue; // Skip empty names + } + name = Some(text); + } + StringType::Message => messages.push(Message::new(text, name.take())), + StringType::Hover => hovers.push(Message::new(text, None)), + StringType::Label => { + if !messages.is_empty() { + let key = label.take().unwrap_or_else(|| "default".to_string()); + if result.contains_key(&key) { + eprintln!( + "Warning: Duplicate label '{}', overwriting previous messages.", + key + ); + crate::COUNTER.inc_warning(); + } + result.insert(key, messages); + messages = Vec::new(); + } + label = Some(text); + } + } + } + if !messages.is_empty() { + let key = label.take().unwrap_or_else(|| "default".to_string()); + result.insert(key, messages); + } + if !hovers.is_empty() { + result.insert("hover".to_string(), hovers); + } + Ok(result) + } + fn import_messages<'a>( &'a self, messages: Vec, @@ -203,6 +256,9 @@ impl Script for SoftpalScript { if addr + 4 > texts_data_len { continue; } + if str.typ.is_label() { + continue; // Ignore labels + } let m = match mess { Some(m) => m, None => return Err(anyhow::anyhow!("Not enough messages.")), @@ -217,6 +273,12 @@ impl Script for SoftpalScript { mess = mes.next(); m } + StringType::Hover => { + let m = m.message.clone(); + mess = mes.next(); + m + } + StringType::Label => continue, // Ignore labels }; if let Some(repl) = replacement { for (from, to) in repl.map.iter() { @@ -260,6 +322,149 @@ impl Script for SoftpalScript { Ok(()) } + fn import_multiple_messages<'a>( + &'a self, + messages: HashMap>, + mut file: Box, + filename: &str, + encoding: Encoding, + replacement: Option<&'a ReplacementTable>, + ) -> Result<()> { + let mut texts_filename = std::path::PathBuf::from(filename); + texts_filename.set_file_name("TEXT.DAT"); + let mut texts = Vec::new(); + let mut reader = self.texts.to_ref(); + reader.pos = 0x10; + while !reader.is_eof() { + reader.pos += 4; // Skip index + texts.push(reader.read_cstring()?) + } + let mut texts_file = std::fs::File::create(&texts_filename) + .map_err(|e| anyhow::anyhow!("Failed to create TEXT.DAT file: {}", e))?; + file.write_all(&self.data.data)?; + let hover_messages = messages.get("hover").cloned().unwrap_or_default(); + let mut hover_iter = hover_messages.iter(); + let mut hover_mes = hover_iter.next(); + let mut cur_label: Option = None; + let mut cur_messages = messages + .get(cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default")) + .cloned() + .unwrap_or_default(); + let mut cur_iter = cur_messages.iter(); + let mut cur_mes = cur_iter.next(); + let texts_data_len = self.texts.data.len() as u32; + let mut num_offset_map: HashMap = HashMap::new(); + for str in &self.strs { + let addr = self.data.cpeek_u32_at(str.offset as u64)?; + if addr + 4 > texts_data_len { + continue; + } + let mut text = match str.typ { + StringType::Label => { + if cur_mes.is_some() || cur_iter.next().is_some() { + return Err(anyhow::anyhow!( + "Not all messages were used for label {}.", + cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default") + )); + } + let text = self.texts.cpeek_cstring_at(addr as u64 + 4)?; + let text = decode_to_string(self.encoding, text.as_bytes(), false)? + .replace("
", "\n"); + cur_messages = messages.get(text.as_str()).cloned().unwrap_or_default(); + cur_iter = cur_messages.iter(); + cur_mes = cur_iter.next(); + cur_label = Some(text); + // We don't need update labels + continue; + } + StringType::Hover => { + let m = match hover_mes { + Some(m) => m, + None => return Err(anyhow::anyhow!("Not enough hover messages.")), + }; + let m = m.message.clone(); + hover_mes = hover_iter.next(); + m + } + StringType::Name => { + let m = match cur_mes { + Some(m) => m, + None => { + return Err(anyhow::anyhow!( + "Not enough messages for label {}.", + cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default") + )); + } + }; + let name = match &m.name { + Some(name) => name.clone(), + None => return Err(anyhow::anyhow!("Missing name for message.")), + }; + name + } + StringType::Message => { + let m = match cur_mes { + Some(m) => m, + None => { + return Err(anyhow::anyhow!( + "Not enough messages for label {}.", + cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default") + )); + } + }; + let m = m.message.clone(); + cur_mes = cur_iter.next(); + m + } + }; + if let Some(repl) = replacement { + for (from, to) in repl.map.iter() { + text = text.replace(from, to); + } + } + text = text.replace("\n", "
"); + let encoded = encode_string(encoding, &text, false)?; + let s = std::ffi::CString::new(encoded)?; + let num = texts.len() as u32; + num_offset_map.insert(num, str.offset); + texts.push(s); + } + if cur_mes.is_some() || cur_iter.next().is_some() { + return Err(anyhow::anyhow!( + "Some messages were not processed for label {}.", + cur_label.as_ref().map(|s| s.as_str()).unwrap_or("default") + )); + } + if hover_mes.is_some() || hover_iter.next().is_some() { + return Err(anyhow::anyhow!("Some hover messages were not processed.")); + } + texts_file.write_all(b"$TEXT_LIST__")?; + texts_file.write_u32(texts.len() as u32)?; + let mut nf = MemWriter::new(); + for (num, text) in texts.into_iter().enumerate() { + let num = num as u32; + let newaddr = nf.pos as u32 + 0x10; + if let Some(offset) = num_offset_map.get(&num) { + file.write_u32_at(*offset as u64, newaddr)?; + } + nf.write_u32(num)?; + nf.write_cstring(&text)?; + } + nf.pos = 0; + let mut shift = 4; + for _ in 0..(nf.data.len() / 4) { + let mut data = nf.cpeek_u32()?; + data ^= 0x084DF873 ^ 0xFF987DEE; + let mut add = data.to_le_bytes(); + add[0] = add[0].rotate_right(shift); + shift = (shift + 1) % 8; + data = u32::from_le_bytes(add); + nf.write_u32(data)?; + } + texts_file.write_all(&nf.data)?; + Ok(()) + } + fn custom_output_extension<'a>(&'a self) -> &'a str { "txt" } diff --git a/src/types.rs b/src/types.rs index 36d2c71..fd4d596 100644 --- a/src/types.rs +++ b/src/types.rs @@ -671,6 +671,7 @@ pub enum FormatType { None, } +#[derive(Clone)] /// Format options pub enum FormatOptions { /// Wrap line with fixed length