mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-07 21:38:58 +08:00
Add multiple part messages support
This commit is contained in:
@@ -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<Vec<Message>> {
|
||||
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<HashMap<String, Vec<Message>>> {
|
||||
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<String, Vec<Message>>,
|
||||
_file: Box<dyn WriteSeek + 'a>,
|
||||
_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<String, Vec<Message>>,
|
||||
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.
|
||||
|
||||
@@ -315,12 +315,23 @@ pub struct Disasm<'a> {
|
||||
variables: HashMap<u32, Operand>,
|
||||
stack: Vec<Operand>,
|
||||
strs: Vec<PalString>,
|
||||
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(());
|
||||
|
||||
@@ -150,6 +150,10 @@ impl Script for SoftpalScript {
|
||||
true
|
||||
}
|
||||
|
||||
fn multiple_message_files(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn extract_messages(&self) -> Result<Vec<Message>> {
|
||||
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<HashMap<String, Vec<Message>>> {
|
||||
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("<br>", "\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<Message>,
|
||||
@@ -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<String, Vec<Message>>,
|
||||
mut file: Box<dyn WriteSeek + 'a>,
|
||||
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<String> = 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<u32, u32> = 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("<br>", "\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", "<br>");
|
||||
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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user