mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-07 05:18:44 +08:00
342 lines
12 KiB
Rust
342 lines
12 KiB
Rust
use crate::ext::io::*;
|
|
use crate::scripts::base::*;
|
|
use crate::types::*;
|
|
use crate::utils::encoding::*;
|
|
use anyhow::Result;
|
|
use fancy_regex::Regex;
|
|
use int_enum::IntEnum;
|
|
use std::io::{Read, Write};
|
|
|
|
#[derive(Debug)]
|
|
pub struct CstScriptBuilder {}
|
|
|
|
impl CstScriptBuilder {
|
|
pub fn new() -> Self {
|
|
CstScriptBuilder {}
|
|
}
|
|
}
|
|
|
|
impl ScriptBuilder for CstScriptBuilder {
|
|
fn default_encoding(&self) -> Encoding {
|
|
Encoding::Cp932
|
|
}
|
|
|
|
fn build_script(
|
|
&self,
|
|
buf: Vec<u8>,
|
|
_filename: &str,
|
|
encoding: Encoding,
|
|
_archive_encoding: Encoding,
|
|
config: &ExtraConfig,
|
|
) -> Result<Box<dyn Script>> {
|
|
Ok(Box::new(CstScript::new(buf, encoding, config)?))
|
|
}
|
|
|
|
fn extensions(&self) -> &'static [&'static str] {
|
|
&["cst"]
|
|
}
|
|
|
|
fn script_type(&self) -> &'static ScriptType {
|
|
&ScriptType::CatSystem
|
|
}
|
|
|
|
fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
|
|
if buf_len >= 8 && buf.starts_with(b"CatScene") {
|
|
return Some(255);
|
|
}
|
|
None
|
|
}
|
|
}
|
|
|
|
trait CustomFn {
|
|
fn write_patched_string(&mut self, s: &CstString, data: &[u8]) -> Result<usize>;
|
|
}
|
|
|
|
impl CustomFn for MemWriter {
|
|
fn write_patched_string(&mut self, s: &CstString, data: &[u8]) -> Result<usize> {
|
|
if data.len() + 1 > s.len {
|
|
let pos = self.data.len();
|
|
self.pos = pos;
|
|
self.write_u8(1)?; // Start marker
|
|
self.write_u8(u8::from(s.typ))?;
|
|
self.write_all(data)?;
|
|
self.write_u8(0)?; // Null terminator
|
|
Ok(pos)
|
|
} else {
|
|
self.pos = s.address;
|
|
self.write_u8(1)?; // Start marker
|
|
self.write_u8(u8::from(s.typ))?;
|
|
self.write_all(data)?;
|
|
self.write_u8(0)?; // Null terminator
|
|
Ok(s.address)
|
|
}
|
|
}
|
|
}
|
|
|
|
#[repr(u8)]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, IntEnum)]
|
|
enum CstStringType {
|
|
EmptyLine = 0x2,
|
|
Paragraph = 0x03,
|
|
Message = 0x20,
|
|
Character = 0x21,
|
|
Command = 0x30,
|
|
FileName = 0xF0,
|
|
LineNumber = 0xF1,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct CstString {
|
|
typ: CstStringType,
|
|
text: String,
|
|
address: usize,
|
|
/// text length (include null terminator)
|
|
len: usize,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct CstScript {
|
|
data: MemReader,
|
|
compressed: bool,
|
|
strings: Vec<CstString>,
|
|
}
|
|
|
|
impl CstScript {
|
|
pub fn new(buf: Vec<u8>, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
|
|
let mut reader = MemReader::new(buf);
|
|
let mut magic = [0; 8];
|
|
reader.read_exact(&mut magic)?;
|
|
if &magic != b"CatScene" {
|
|
return Err(anyhow::anyhow!("Invalid CST script magic: {:?}", magic));
|
|
}
|
|
let compressed_size = reader.read_u32()?;
|
|
let uncompressed_size = reader.read_u32()?;
|
|
let mut file = if compressed_size == 0 {
|
|
if uncompressed_size != reader.data.len() as u32 - 0x10 {
|
|
return Err(anyhow::anyhow!(
|
|
"Uncompressed size mismatch: expected {}, got {}",
|
|
uncompressed_size,
|
|
reader.data.len() as u32 - 0x10
|
|
));
|
|
}
|
|
MemReader::new((&reader.data[0x10..]).to_vec())
|
|
} else {
|
|
let mut decoder = flate2::read::ZlibDecoder::new(reader);
|
|
let mut data = Vec::with_capacity(uncompressed_size as usize);
|
|
decoder.read_to_end(&mut data)?;
|
|
MemReader::new(data)
|
|
};
|
|
let data_length = file.read_u32()?;
|
|
if data_length as usize + 0x10 != file.data.len() {
|
|
return Err(anyhow::anyhow!(
|
|
"Data length mismatch: expected {}, got {}",
|
|
data_length,
|
|
file.data.len() - 0x10
|
|
));
|
|
}
|
|
let _clear_screen_count = file.read_u32()?;
|
|
let string_address_offset = 0x10 + file.read_u32()?;
|
|
let strings_offset = 0x10 + file.read_u32()?;
|
|
let string_count = (strings_offset - string_address_offset) / 4;
|
|
let mut strings = Vec::with_capacity(string_count as usize);
|
|
for i in 0..string_count {
|
|
let offset = file.cpeek_u32_at(string_address_offset as usize + i as usize * 4)?
|
|
as usize
|
|
+ strings_offset as usize;
|
|
file.pos = offset;
|
|
let start_marker = file.read_u8()?;
|
|
if start_marker != 1 {
|
|
return Err(anyhow::anyhow!(
|
|
"Invalid start marker for string {}: expected 0x01, got {:02X}",
|
|
i,
|
|
start_marker
|
|
));
|
|
}
|
|
let typ = CstStringType::try_from(file.read_u8()?).map_err(|code| {
|
|
anyhow::anyhow!("Invalid string type for string {}: {:02X}", i, code)
|
|
})?;
|
|
let str = file.read_cstring()?;
|
|
let text = decode_to_string(encoding, str.as_bytes(), true)?;
|
|
strings.push(CstString {
|
|
typ,
|
|
text,
|
|
address: offset,
|
|
len: str.as_bytes_with_nul().len(),
|
|
});
|
|
}
|
|
Ok(CstScript {
|
|
data: file,
|
|
compressed: compressed_size != 0,
|
|
strings,
|
|
})
|
|
}
|
|
}
|
|
|
|
lazy_static::lazy_static! {
|
|
static ref CST_COMMAND_REGEX: Regex = Regex::new(r"^\d+\s+\w+\s+(.+)").unwrap();
|
|
}
|
|
|
|
impl Script for CstScript {
|
|
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 mut name = None;
|
|
for s in self.strings.iter() {
|
|
match s.typ {
|
|
CstStringType::Message => {
|
|
if s.text.is_empty() {
|
|
continue; // Skip empty messages
|
|
}
|
|
messages.push(Message {
|
|
message: s.text.replace("\\n", "\n"),
|
|
name: name.take(),
|
|
});
|
|
}
|
|
CstStringType::Character => {
|
|
name = Some(s.text.clone());
|
|
}
|
|
CstStringType::Command => {
|
|
if let Some(caps) = CST_COMMAND_REGEX.captures(&s.text)? {
|
|
if let Some(text) = caps.get(1) {
|
|
messages.push(Message {
|
|
message: text.as_str().to_string(),
|
|
name: None,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
Ok(messages)
|
|
}
|
|
|
|
fn import_messages<'a>(
|
|
&'a self,
|
|
messages: Vec<Message>,
|
|
mut file: Box<dyn WriteSeek + 'a>,
|
|
encoding: Encoding,
|
|
replacement: Option<&'a ReplacementTable>,
|
|
) -> Result<()> {
|
|
let mut writer = MemWriter::from_vec(self.data.data.clone());
|
|
let mut mess = messages.iter();
|
|
let mut mes = mess.next();
|
|
let strings_address_offset = 0x10 + self.data.cpeek_u32_at(0x8)? as usize;
|
|
let strings_offset = 0x10 + self.data.cpeek_u32_at(0xC)? as usize;
|
|
for (i, s) in self.strings.iter().enumerate() {
|
|
match s.typ {
|
|
CstStringType::Message => {
|
|
if s.text.is_empty() {
|
|
continue; // Skip empty messages
|
|
}
|
|
let m = match mes {
|
|
Some(m) => m,
|
|
None => {
|
|
return Err(anyhow::anyhow!("No enough messages."));
|
|
}
|
|
};
|
|
let mut message = m.message.clone();
|
|
if let Some(replacement) = replacement {
|
|
for (k, v) in &replacement.map {
|
|
message = message.replace(k, v);
|
|
}
|
|
}
|
|
message = message.replace("\n", "\\n");
|
|
let data = encode_string(encoding, &message, true)?;
|
|
let pos = writer.write_patched_string(s, &data)?;
|
|
if pos != s.address {
|
|
writer.write_u32_at(
|
|
strings_address_offset + i * 4,
|
|
(pos - strings_offset) as u32,
|
|
)?;
|
|
}
|
|
mes = mess.next();
|
|
}
|
|
CstStringType::Character => {
|
|
let m = match mes {
|
|
Some(m) => m,
|
|
None => {
|
|
return Err(anyhow::anyhow!("No enough messages."));
|
|
}
|
|
};
|
|
let mut name = match &m.name {
|
|
Some(name) => name.to_owned(),
|
|
None => return Err(anyhow::anyhow!("Message without name.")),
|
|
};
|
|
if let Some(replacement) = replacement {
|
|
for (k, v) in &replacement.map {
|
|
name = name.replace(k, v);
|
|
}
|
|
}
|
|
let data = encode_string(encoding, &name, true)?;
|
|
let pos = writer.write_patched_string(s, &data)?;
|
|
if pos != s.address {
|
|
writer.write_u32_at(
|
|
strings_address_offset + i * 4,
|
|
(pos - strings_offset) as u32,
|
|
)?;
|
|
}
|
|
}
|
|
CstStringType::Command => {
|
|
if let Some(caps) = CST_COMMAND_REGEX.captures(&s.text)? {
|
|
if let Some(mat) = caps.get(1) {
|
|
let m = match mes {
|
|
Some(m) => m,
|
|
None => {
|
|
return Err(anyhow::anyhow!("No enough messages."));
|
|
}
|
|
};
|
|
let mut text = m.message.clone();
|
|
if let Some(replacement) = replacement {
|
|
for (k, v) in &replacement.map {
|
|
text = text.replace(k, v);
|
|
}
|
|
}
|
|
let mut command_text = s.text.clone();
|
|
command_text.replace_range(mat.range(), &text);
|
|
let data = encode_string(encoding, &command_text, true)?;
|
|
let pos = writer.write_patched_string(s, &data)?;
|
|
if pos != s.address {
|
|
writer.write_u32_at(
|
|
strings_address_offset + i * 4,
|
|
(pos - strings_offset) as u32,
|
|
)?;
|
|
}
|
|
mes = mess.next();
|
|
}
|
|
}
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
if mes.is_some() || mess.next().is_some() {
|
|
return Err(anyhow::anyhow!("Not all messages were processed."));
|
|
}
|
|
let data_len = writer.data.len() as u32 - 0x10;
|
|
writer.write_u32_at(0, data_len)?;
|
|
let data = writer.into_inner();
|
|
file.write_all(b"CatScene")?;
|
|
file.write_u32(0)?; // Compressed size
|
|
file.write_u32(data.len() as u32)?; // Uncompressed size
|
|
if self.compressed {
|
|
let mut encoder =
|
|
flate2::write::ZlibEncoder::new(&mut file, flate2::Compression::default());
|
|
encoder.write_all(&data)?;
|
|
encoder.finish()?;
|
|
let file_len = file.stream_position()?;
|
|
let compressed_size = (file_len as u32) - 0x10;
|
|
file.write_u32_at(8, compressed_size)?;
|
|
} else {
|
|
file.write_all(&data)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|