mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-06-07 21:38:58 +08:00
Add Favorite Hcb Script (.hcb) support
This commit is contained in:
174
src/scripts/favorite/disasm.rs
Normal file
174
src/scripts/favorite/disasm.rs
Normal file
@@ -0,0 +1,174 @@
|
||||
use crate::ext::io::*;
|
||||
use crate::types::*;
|
||||
use crate::utils::encoding::*;
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Read, Seek, SeekFrom};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Oper {
|
||||
// Byte
|
||||
B,
|
||||
// Word
|
||||
W,
|
||||
// Double Word
|
||||
D,
|
||||
// String
|
||||
S,
|
||||
}
|
||||
|
||||
use Oper::*;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(tag = "t", content = "c")]
|
||||
pub enum Operand {
|
||||
B(u8),
|
||||
W(u16),
|
||||
D(u32),
|
||||
S(String),
|
||||
}
|
||||
|
||||
impl Operand {
|
||||
pub fn len(&self, encoding: Encoding) -> Result<usize> {
|
||||
Ok(match self {
|
||||
Operand::B(_) => 1,
|
||||
Operand::W(_) => 2,
|
||||
Operand::D(_) => 4,
|
||||
Operand::S(s) => {
|
||||
let bytes = encode_string(encoding, s, true)?;
|
||||
// null terminator + length byte
|
||||
bytes.len() + 2
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const OPS: [(u8, &[Oper]); 49] = [
|
||||
(0x00, &[]),
|
||||
(0x01, &[B, B]), //unknown
|
||||
(0x02, &[D]), //call function
|
||||
(0x03, &[W]), //unknown
|
||||
(0x04, &[]), //retn?
|
||||
(0x05, &[]), //retn?
|
||||
(0x06, &[D]), //jump?
|
||||
(0x07, &[D]), //cond jump?
|
||||
(0x08, &[]), //unknown
|
||||
(0x09, &[]), //unknown
|
||||
(0x0a, &[D]), //unknown
|
||||
(0x0b, &[W]), //unknown
|
||||
(0x0c, &[B]), //unknown
|
||||
(0x0d, &[]), //empty
|
||||
(0x0e, &[S]), //string
|
||||
(0x0f, &[W]), //unknown
|
||||
(0x10, &[B]), //unknown
|
||||
(0x11, &[W]), //unknown
|
||||
(0x12, &[B]), //unknown
|
||||
(0x13, &[]),
|
||||
(0x14, &[]), //unknown
|
||||
(0x15, &[W]), //unknown
|
||||
(0x16, &[B]), //unknown
|
||||
(0x17, &[W]), //unknown
|
||||
(0x18, &[B]), //unknown
|
||||
(0x19, &[]), //unknown
|
||||
(0x1a, &[]), //unknown
|
||||
(0x1b, &[]), //unknown
|
||||
(0x1c, &[]), //unknown
|
||||
(0x1d, &[]), //unknown
|
||||
(0x1e, &[]), //unknown
|
||||
(0x1f, &[]), //unknown
|
||||
(0x20, &[]), //unknown
|
||||
(0x21, &[]), //unknown
|
||||
(0x22, &[]), //unknown
|
||||
(0x23, &[]), //unknown
|
||||
(0x24, &[]), //unknown
|
||||
(0x25, &[]), //unknown
|
||||
(0x26, &[]), //unknown
|
||||
(0x27, &[]), //unknown
|
||||
(0x33, &[]),
|
||||
(0x3f, &[]),
|
||||
(0x40, &[]),
|
||||
(0xb3, &[]),
|
||||
(0xb8, &[]),
|
||||
(0xd8, &[]),
|
||||
(0xf0, &[]),
|
||||
(0x52, &[]),
|
||||
(0x9e, &[]),
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Func {
|
||||
pub pos: u64,
|
||||
pub opcode: u8,
|
||||
pub operands: Vec<Operand>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Data {
|
||||
pub functions: Vec<Func>,
|
||||
pub main_script: Vec<Func>,
|
||||
pub extra_data: Vec<u8>,
|
||||
}
|
||||
|
||||
impl Data {
|
||||
pub fn disasm<R: Read + Seek>(mut reader: R, encoding: Encoding) -> Result<Self> {
|
||||
let mut data = Data {
|
||||
functions: Vec::new(),
|
||||
main_script: Vec::new(),
|
||||
extra_data: Vec::new(),
|
||||
};
|
||||
let script_len = reader.read_u32()? as u64;
|
||||
let main_script_data = reader.peek_u32_at(script_len)? as u64;
|
||||
{
|
||||
let mut target = &mut data.functions;
|
||||
let mut pos = reader.stream_position()?;
|
||||
while pos < script_len {
|
||||
if pos >= main_script_data {
|
||||
target = &mut data.main_script;
|
||||
}
|
||||
target.push(Self::read_func(&mut reader, encoding)?);
|
||||
pos = reader.stream_position()?;
|
||||
}
|
||||
}
|
||||
reader.seek(SeekFrom::Start(script_len + 4))?;
|
||||
reader.read_to_end(&mut data.extra_data)?;
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn read_func<R: Read + Seek>(reader: &mut R, encoding: Encoding) -> Result<Func> {
|
||||
let pos = reader.stream_position()?;
|
||||
let opcode = reader.read_u8()?;
|
||||
let operands = if let Some((_, ops)) = OPS.iter().find(|(code, _)| *code == opcode) {
|
||||
let mut operands = Vec::with_capacity(ops.len());
|
||||
for &op in *ops {
|
||||
let operand = match op {
|
||||
B => Operand::B(reader.read_u8()?),
|
||||
W => Operand::W(reader.read_u16()?),
|
||||
D => Operand::D(reader.read_u32()?),
|
||||
S => {
|
||||
let len = reader.read_u8()? as usize;
|
||||
let s = reader.read_cstring()?;
|
||||
if s.as_bytes_with_nul().len() != len {
|
||||
return Err(anyhow::anyhow!(
|
||||
"String length mismatch at {:#x}: expected {}, got {}",
|
||||
pos,
|
||||
len,
|
||||
s.as_bytes_with_nul().len()
|
||||
));
|
||||
}
|
||||
let s = decode_to_string(encoding, s.as_bytes(), true)?;
|
||||
Operand::S(s)
|
||||
}
|
||||
};
|
||||
operands.push(operand);
|
||||
}
|
||||
operands
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("Unknown opcode: {:#x} at {:#x}", opcode, pos))
|
||||
};
|
||||
Ok(Func {
|
||||
pos,
|
||||
opcode,
|
||||
operands,
|
||||
})
|
||||
}
|
||||
}
|
||||
182
src/scripts/favorite/hcb.rs
Normal file
182
src/scripts/favorite/hcb.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
//! Favorite HCB script (.hcb)
|
||||
use super::disasm::*;
|
||||
use crate::ext::io::*;
|
||||
use crate::scripts::base::*;
|
||||
use crate::types::*;
|
||||
use crate::utils::encoding::*;
|
||||
use anyhow::Result;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Favorite HCB script builder
|
||||
pub struct HcbScriptBuilder {}
|
||||
|
||||
impl HcbScriptBuilder {
|
||||
/// Create a new HcbScriptBuilder
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScriptBuilder for HcbScriptBuilder {
|
||||
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(HcbScript::new(buf, encoding, config)?))
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["hcb"]
|
||||
}
|
||||
|
||||
fn script_type(&self) -> &'static ScriptType {
|
||||
&ScriptType::Favorite
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct HcbScript {
|
||||
data: Data,
|
||||
reader: MemReader,
|
||||
custom_yaml: bool,
|
||||
filter_ascii: bool,
|
||||
encoding: Encoding,
|
||||
}
|
||||
|
||||
impl HcbScript {
|
||||
pub fn new(buf: Vec<u8>, encoding: Encoding, config: &ExtraConfig) -> Result<Self> {
|
||||
let reader = MemReader::new(buf);
|
||||
let data = Data::disasm(reader.to_ref(), encoding)?;
|
||||
Ok(Self {
|
||||
data,
|
||||
reader,
|
||||
custom_yaml: config.custom_yaml,
|
||||
filter_ascii: config.favorite_hcb_filter_ascii,
|
||||
encoding,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Script for HcbScript {
|
||||
fn default_output_script_type(&self) -> OutputScriptType {
|
||||
OutputScriptType::Json
|
||||
}
|
||||
|
||||
fn default_format_type(&self) -> FormatOptions {
|
||||
FormatOptions::None
|
||||
}
|
||||
|
||||
fn is_output_supported(&self, _: OutputScriptType) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn custom_output_extension<'a>(&'a self) -> &'a str {
|
||||
if self.custom_yaml { "yaml" } else { "json" }
|
||||
}
|
||||
|
||||
fn extract_messages(&self) -> Result<Vec<Message>> {
|
||||
let mut messages = Vec::new();
|
||||
for funcs in [&self.data.functions, &self.data.main_script] {
|
||||
for func in funcs {
|
||||
for operand in &func.operands {
|
||||
if let Operand::S(s) = operand {
|
||||
if self.filter_ascii && s.chars().all(|c| c.is_ascii()) {
|
||||
continue;
|
||||
}
|
||||
messages.push(Message::new(s.clone(), None));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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 mut mess = messages.iter();
|
||||
let mut mes = mess.next();
|
||||
let mut patcher =
|
||||
BinaryPatcher::new(self.reader.to_ref(), file, |pos| Ok(pos), |pos| Ok(pos))?;
|
||||
let mut need_pacth_addresses = Vec::new();
|
||||
for funcs in [&self.data.functions, &self.data.main_script] {
|
||||
for func in funcs {
|
||||
let mut cur_pos = func.pos + 1;
|
||||
if matches!(func.opcode, 0x02 | 0x06 | 0x07) {
|
||||
need_pacth_addresses.push(cur_pos);
|
||||
}
|
||||
for operand in &func.operands {
|
||||
if let Operand::S(s) = operand {
|
||||
if self.filter_ascii && s.chars().all(|c| c.is_ascii()) {
|
||||
continue;
|
||||
}
|
||||
let m = match mes {
|
||||
Some(m) => m,
|
||||
None => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Not enough messages to import. Missing message: {}",
|
||||
s
|
||||
));
|
||||
}
|
||||
};
|
||||
let mut message = m.message.clone();
|
||||
if let Some(table) = replacement {
|
||||
for (k, v) in &table.map {
|
||||
message = message.replace(k, v);
|
||||
}
|
||||
}
|
||||
patcher.copy_up_to(cur_pos)?;
|
||||
let ori_len = operand.len(self.encoding)? as u64;
|
||||
let mut s = encode_string(encoding, &message, true)?;
|
||||
s.push(0); // null-terminated
|
||||
let len = s.len();
|
||||
if len > 255 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"Message too long to import (max 255 bytes): {}",
|
||||
message
|
||||
));
|
||||
}
|
||||
patcher.replace_bytes_with_write(ori_len, |writer| {
|
||||
writer.write_u8(len as u8)?;
|
||||
writer.write_all(&s)?;
|
||||
Ok(())
|
||||
})?;
|
||||
mes = mess.next();
|
||||
}
|
||||
cur_pos += operand.len(self.encoding)? as u64;
|
||||
}
|
||||
}
|
||||
}
|
||||
patcher.copy_up_to(self.reader.data.len() as u64)?;
|
||||
for addr in need_pacth_addresses {
|
||||
patcher.patch_u32_address(addr)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> {
|
||||
let s = if self.custom_yaml {
|
||||
serde_yaml_ng::to_string(&self.data)?
|
||||
} else {
|
||||
serde_json::to_string_pretty(&self.data)?
|
||||
};
|
||||
let e = encode_string(encoding, &s, false)?;
|
||||
let mut writer = crate::utils::files::write_file(filename)?;
|
||||
writer.write_all(&e)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
3
src/scripts/favorite/mod.rs
Normal file
3
src/scripts/favorite/mod.rs
Normal file
@@ -0,0 +1,3 @@
|
||||
//! FAVORITE scripts
|
||||
mod disasm;
|
||||
pub mod hcb;
|
||||
Reference in New Issue
Block a user