mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-07 21:38:58 +08:00
427 lines
14 KiB
Rust
427 lines
14 KiB
Rust
use super::disasm::*;
|
|
use crate::ext::io::*;
|
|
use crate::scripts::base::*;
|
|
use crate::types::*;
|
|
use crate::utils::encoding::*;
|
|
use anyhow::Result;
|
|
use std::cell::RefCell;
|
|
use std::io::Write;
|
|
use unicode_segmentation::UnicodeSegmentation;
|
|
|
|
#[derive(Debug)]
|
|
/// Sliky mes script builder
|
|
pub struct MesBuilder {}
|
|
|
|
impl MesBuilder {
|
|
/// Create a new Sliky mes script builder
|
|
pub fn new() -> Self {
|
|
Self {}
|
|
}
|
|
}
|
|
|
|
impl ScriptBuilder for MesBuilder {
|
|
fn default_encoding(&self) -> Encoding {
|
|
Encoding::Cp932
|
|
}
|
|
|
|
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(Mes::new(buf, encoding, config)?))
|
|
}
|
|
|
|
fn extensions(&self) -> &'static [&'static str] {
|
|
&["mes"]
|
|
}
|
|
|
|
fn script_type(&self) -> &'static ScriptType {
|
|
&ScriptType::Silky
|
|
}
|
|
}
|
|
|
|
struct TextParser<'a> {
|
|
data: Vec<&'a str>,
|
|
typ: SlikyStringType,
|
|
opcodes: &'static Opcodes,
|
|
encoding: Encoding,
|
|
pos: usize,
|
|
}
|
|
|
|
impl<'a> TextParser<'a> {
|
|
fn new(
|
|
s: &'a str,
|
|
typ: SlikyStringType,
|
|
opcodes: &'static Opcodes,
|
|
encoding: Encoding,
|
|
) -> Self {
|
|
let data = s.graphemes(true).collect();
|
|
Self {
|
|
data,
|
|
typ,
|
|
opcodes,
|
|
encoding,
|
|
pos: 0,
|
|
}
|
|
}
|
|
|
|
fn parse(mut self) -> Result<Vec<u8>> {
|
|
match self.typ {
|
|
SlikyStringType::Internal => Err(anyhow::anyhow!(
|
|
"Internal strings cannot be parsed from text."
|
|
)),
|
|
SlikyStringType::Name => {
|
|
let mut m = MemWriter::new();
|
|
m.write_u8(self.opcodes.push_string)?;
|
|
let s = encode_string(self.encoding, &self.data.join(""), false)?;
|
|
m.write_all(&s)?;
|
|
m.write_u8(0)?;
|
|
Ok(m.into_inner())
|
|
}
|
|
SlikyStringType::Message => {
|
|
let mut m = MemWriter::new();
|
|
let mut in_ruby = false;
|
|
let mut in_normal_text = false;
|
|
while let Some(c) = self.next() {
|
|
match c {
|
|
"[" => {
|
|
if in_ruby {
|
|
return Err(anyhow::anyhow!("Nested ruby tags are not allowed."));
|
|
}
|
|
if in_normal_text {
|
|
m.write_u8(0)?;
|
|
in_normal_text = false;
|
|
}
|
|
in_ruby = true;
|
|
m.write_u8(self.opcodes.escape_sequence)?;
|
|
m.write_u8(1)?; // ruby start
|
|
m.write_u8(self.opcodes.message2)?;
|
|
}
|
|
"]" => {
|
|
if !in_ruby {
|
|
return Err(anyhow::anyhow!("Unmatched closing ruby tag."));
|
|
}
|
|
in_ruby = false;
|
|
m.write_u8(0)?;
|
|
m.write_u8(self.opcodes.r#yield)?;
|
|
}
|
|
"\n" => {
|
|
if in_ruby {
|
|
return Err(anyhow::anyhow!("Newline inside ruby is not allowed."));
|
|
}
|
|
if in_normal_text {
|
|
m.write_u8(0)?;
|
|
in_normal_text = false;
|
|
}
|
|
m.write_u8(self.opcodes.escape_sequence)?;
|
|
m.write_u8(0)?; // new line
|
|
}
|
|
_ => {
|
|
if !in_ruby && !in_normal_text {
|
|
in_normal_text = true;
|
|
m.write_u8(self.opcodes.message2)?;
|
|
}
|
|
let s = encode_string(self.encoding, c, false)?;
|
|
m.write_all(&s)?;
|
|
}
|
|
}
|
|
}
|
|
if in_ruby {
|
|
m.write_u8(0)?;
|
|
m.write_u8(self.opcodes.r#yield)?;
|
|
}
|
|
if in_normal_text {
|
|
m.write_u8(0)?;
|
|
}
|
|
Ok(m.into_inner())
|
|
}
|
|
}
|
|
}
|
|
|
|
fn next(&mut self) -> Option<&'a str> {
|
|
if self.pos < self.data.len() {
|
|
let c = self.data[self.pos];
|
|
self.pos += 1;
|
|
Some(c)
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Mes {
|
|
disasm: RefCell<Box<dyn Disasm>>,
|
|
encoding: Encoding,
|
|
texts: Vec<SlikyString>,
|
|
}
|
|
|
|
impl Mes {
|
|
pub fn new(buf: Vec<u8>, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
|
|
let reader = MemReader::new(buf);
|
|
let num_message = reader.cpeek_u32()?;
|
|
let code_offset = 4 + num_message as u64 * 4;
|
|
let first_line_offset = reader.cpeek_u32_at(4)? as u64 + code_offset;
|
|
let mut disasm: Box<dyn Disasm> = if reader.cpeek_u8_at(first_line_offset)? == 0x19
|
|
&& reader.cpeek_u32_at(first_line_offset + 1)? == 0
|
|
{
|
|
Box::new(Ai6WinDisasm::new(reader)?)
|
|
} else {
|
|
Box::new(PlusDisasm::new(reader)?)
|
|
};
|
|
disasm.read_header()?;
|
|
let texts = disasm.read_code()?;
|
|
Ok(Self {
|
|
disasm: RefCell::new(disasm),
|
|
encoding,
|
|
texts,
|
|
})
|
|
}
|
|
|
|
fn code_to_text(&self, str: &SlikyString) -> Result<String> {
|
|
let mut disasm = self.disasm.try_borrow_mut()?;
|
|
let mut result = String::new();
|
|
disasm.stream_mut().pos = str.start as usize;
|
|
let end = str.start as usize + str.len as usize;
|
|
let opcodes = disasm.opcodes();
|
|
while disasm.stream().pos < end {
|
|
let (opcode, operands) = disasm.read_instruction()?;
|
|
if opcode == opcodes.push_string
|
|
|| (opcode == opcodes.message1 && !opcodes.is_message1_obfuscated)
|
|
|| opcode == opcodes.message2
|
|
{
|
|
if let Some(Obj::Str(s)) = operands.get(0) {
|
|
let s = disasm.stream().cpeek_fstring_at(
|
|
s.start,
|
|
s.len as usize,
|
|
self.encoding,
|
|
true,
|
|
)?;
|
|
result.push_str(&s);
|
|
}
|
|
} else if opcode == opcodes.message1 && opcodes.is_message1_obfuscated {
|
|
if let Some(Obj::Str(s)) = operands.get(0) {
|
|
let mut deobfuscated = vec![0u8; (s.len as usize - 1) * 2];
|
|
let mut input_idx = 0;
|
|
let mut output_idx = 0;
|
|
let tlen = s.len - 1;
|
|
while input_idx < tlen {
|
|
let b = disasm.stream().cpeek_u8_at(s.start + input_idx)?;
|
|
input_idx += 1;
|
|
if matches!(b, 0x81..0xA0 | 0xE0..0xF0) {
|
|
deobfuscated[output_idx] = b;
|
|
output_idx += 1;
|
|
deobfuscated[output_idx] =
|
|
disasm.stream().cpeek_u8_at(s.start + input_idx)?;
|
|
input_idx += 1;
|
|
output_idx += 1;
|
|
} else {
|
|
let c = b as i32 - 0x7D62;
|
|
deobfuscated[output_idx] = (c >> 8) as u8;
|
|
output_idx += 1;
|
|
deobfuscated[output_idx] = (c & 0xFF) as u8;
|
|
output_idx += 1;
|
|
}
|
|
}
|
|
deobfuscated.truncate(output_idx);
|
|
let s = decode_to_string(self.encoding, &deobfuscated, true)?;
|
|
result.push_str(&s);
|
|
}
|
|
} else if opcode == opcodes.escape_sequence {
|
|
if let Some(Obj::Byte(e)) = operands.get(0) {
|
|
match e {
|
|
// new line
|
|
0 => result.push('\n'),
|
|
// ruby
|
|
1 => result.push_str("["),
|
|
_ => {
|
|
return Err(anyhow::anyhow!("Unknown escape sequence: {}", e));
|
|
}
|
|
}
|
|
}
|
|
} else if opcode == opcodes.r#yield {
|
|
result.push_str("]");
|
|
}
|
|
}
|
|
Ok(result)
|
|
}
|
|
}
|
|
|
|
impl Script for Mes {
|
|
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 t in self.texts.iter() {
|
|
match t.typ {
|
|
SlikyStringType::Internal => {}
|
|
SlikyStringType::Name => {
|
|
name = Some(self.code_to_text(t)?);
|
|
}
|
|
SlikyStringType::Message => {
|
|
let message = self.code_to_text(t)?;
|
|
messages.push(Message {
|
|
name: name.take(),
|
|
message,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
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 opcodes = self.disasm.try_borrow()?.opcodes();
|
|
let mut inp = self.disasm.try_borrow()?.stream().clone();
|
|
inp.pos = 0;
|
|
let mut patcher = BinaryPatcher::new(inp.to_ref(), file, |add| Ok(add), |add| Ok(add))?;
|
|
let mut mess = messages.iter();
|
|
let mut mes = mess.next();
|
|
for text in &self.texts {
|
|
patcher.copy_up_to(text.start)?;
|
|
match text.typ {
|
|
// Ignore internal strings
|
|
SlikyStringType::Internal => {}
|
|
SlikyStringType::Name => {
|
|
let m = match mes {
|
|
Some(m) => m,
|
|
None => {
|
|
return Err(anyhow::anyhow!("Not enough messages"));
|
|
}
|
|
};
|
|
let mut name = match &m.name {
|
|
Some(n) => n.to_string(),
|
|
None => {
|
|
return Err(anyhow::anyhow!("Message name is missing"));
|
|
}
|
|
};
|
|
if let Some(repl) = replacement {
|
|
for (k, v) in &repl.map {
|
|
name = name.replace(k, v);
|
|
}
|
|
}
|
|
let data =
|
|
TextParser::new(&name, SlikyStringType::Name, opcodes, encoding).parse()?;
|
|
patcher.replace_bytes(text.len, &data)?;
|
|
}
|
|
SlikyStringType::Message => {
|
|
let m = match mes {
|
|
Some(m) => m,
|
|
None => {
|
|
return Err(anyhow::anyhow!("Not enough messages"));
|
|
}
|
|
};
|
|
let mut message = m.message.to_string();
|
|
if let Some(repl) = replacement {
|
|
for (k, v) in &repl.map {
|
|
message = message.replace(k, v);
|
|
}
|
|
}
|
|
let data =
|
|
TextParser::new(&message, SlikyStringType::Message, opcodes, encoding)
|
|
.parse()?;
|
|
patcher.replace_bytes(text.len, &data)?;
|
|
mes = mess.next();
|
|
}
|
|
}
|
|
}
|
|
if mes.is_some() || mess.next().is_some() {
|
|
return Err(anyhow::anyhow!("Too many messages"));
|
|
}
|
|
patcher.copy_up_to(inp.data.len() as u64)?;
|
|
let code_offset = self.disasm.try_borrow()?.code_offset();
|
|
for &address_offset in self.disasm.try_borrow()?.little_endian_addresses() {
|
|
let orig_address = inp.cpeek_u32_at(address_offset as u64)? as u64;
|
|
let orig_offset = orig_address + code_offset as u64;
|
|
let new_offset = patcher.map_offset(orig_offset)?;
|
|
let new_address = new_offset - code_offset as u64;
|
|
patcher.patch_u32(address_offset as u64, new_address as u32)?;
|
|
}
|
|
for &address_offset in self.disasm.try_borrow()?.big_endian_addresses() {
|
|
let orig_address = inp.cpeek_u32_be_at(address_offset as u64)? as u64;
|
|
let orig_offset = orig_address + code_offset as u64;
|
|
let new_offset = patcher.map_offset(orig_offset)?;
|
|
let new_address = new_offset - code_offset as u64;
|
|
patcher.patch_u32_be(address_offset as u64, new_address as u32)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_text_parser() {
|
|
let opcodes = &PLUS_OPCODES;
|
|
let parser = TextParser::new(
|
|
"Hello, [world]s\nThis is a test.",
|
|
SlikyStringType::Message,
|
|
opcodes,
|
|
Encoding::Utf8,
|
|
);
|
|
let data = parser.parse().unwrap();
|
|
assert_eq!(
|
|
data,
|
|
vec![
|
|
opcodes.message2,
|
|
b'H',
|
|
b'e',
|
|
b'l',
|
|
b'l',
|
|
b'o',
|
|
b',',
|
|
b' ',
|
|
0,
|
|
opcodes.escape_sequence,
|
|
1,
|
|
opcodes.message2,
|
|
b'w',
|
|
b'o',
|
|
b'r',
|
|
b'l',
|
|
b'd',
|
|
0,
|
|
opcodes.r#yield,
|
|
opcodes.message2,
|
|
b's',
|
|
0,
|
|
opcodes.escape_sequence,
|
|
0,
|
|
opcodes.message2,
|
|
b'T',
|
|
b'h',
|
|
b'i',
|
|
b's',
|
|
b' ',
|
|
b'i',
|
|
b's',
|
|
b' ',
|
|
b'a',
|
|
b' ',
|
|
b't',
|
|
b'e',
|
|
b's',
|
|
b't',
|
|
b'.',
|
|
0
|
|
]
|
|
);
|
|
}
|