Add escude script export support

This commit is contained in:
2025-06-02 21:38:38 +08:00
parent 24192050dc
commit abf30c8a55
14 changed files with 1454 additions and 27 deletions

View File

@@ -10,7 +10,7 @@ pub trait ScriptBuilder: std::fmt::Debug {
fn build_script(
&self,
filename: &str,
buf: Vec<u8>,
encoding: Encoding,
config: &ExtraConfig,
) -> Result<Box<dyn Script>>;
@@ -24,18 +24,48 @@ pub trait ScriptBuilder: std::fmt::Debug {
fn script_type(&self) -> &'static ScriptType;
}
pub trait ArchiveContent {
fn name(&self) -> &str;
fn data(&self) -> &[u8];
fn is_script(&self) -> bool;
}
pub trait Script: std::fmt::Debug {
fn default_output_script_type(&self) -> OutputScriptType;
fn default_format_type(&self) -> FormatOptions;
fn extract_messages(&self) -> Result<Vec<Message>>;
fn extract_messages(&self) -> Result<Vec<Message>> {
if !self.is_archive() {
return Err(anyhow::anyhow!(
"This script type does not support extracting messages."
));
}
Ok(vec![])
}
fn import_messages(
&self,
messages: Vec<Message>,
filename: &str,
encoding: Encoding,
replacement: Option<&ReplacementTable>,
) -> Result<()>;
_messages: Vec<Message>,
_filename: &str,
_encoding: Encoding,
_replacement: Option<&ReplacementTable>,
) -> Result<()> {
if !self.is_archive() {
return Err(anyhow::anyhow!(
"This script type does not support importing messages."
));
}
Ok(())
}
fn is_archive(&self) -> bool {
false
}
fn iter_archive<'a>(
&'a self,
) -> Result<Box<dyn Iterator<Item = Result<Box<dyn ArchiveContent>>> + 'a>> {
Ok(Box::new(std::iter::empty()))
}
}

View File

@@ -20,15 +20,11 @@ impl ScriptBuilder for BGIScriptBuilder {
fn build_script(
&self,
filename: &str,
buf: Vec<u8>,
encoding: Encoding,
config: &ExtraConfig,
) -> Result<Box<dyn Script>> {
Ok(Box::new(BGIScript::new(
filename.as_ref(),
encoding,
config,
)?))
Ok(Box::new(BGIScript::new(buf, encoding, config)?))
}
fn extensions(&self) -> &'static [&'static str] {
@@ -64,8 +60,7 @@ impl std::fmt::Debug for BGIScript {
}
impl BGIScript {
pub fn new(filename: &str, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
let data = crate::utils::files::read_file(filename)?;
pub fn new(data: Vec<u8>, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
if data.starts_with(b"BurikoCompiledScriptVer1.00\0") {
let mut parser = V1Parser::new(&data, encoding)?;
parser.disassemble()?;

View File

@@ -20,15 +20,11 @@ impl ScriptBuilder for CircusMesScriptBuilder {
fn build_script(
&self,
filename: &str,
buf: Vec<u8>,
encoding: Encoding,
config: &ExtraConfig,
) -> Result<Box<dyn Script>> {
Ok(Box::new(CircusMesScript::new(
filename.as_ref(),
encoding,
config,
)?))
Ok(Box::new(CircusMesScript::new(buf, encoding, config)?))
}
fn extensions(&self) -> &'static [&'static str] {
@@ -59,8 +55,7 @@ pub struct CircusMesScript {
}
impl CircusMesScript {
pub fn new(filename: &str, encoding: Encoding, config: &ExtraConfig) -> Result<Self> {
let data = crate::utils::files::read_file(filename)?;
pub fn new(data: Vec<u8>, encoding: Encoding, config: &ExtraConfig) -> Result<Self> {
let head0 = i32::from_le_bytes(data[0..4].try_into()?);
let head1 = i32::from_le_bytes(data[4..8].try_into()?);
let mut is_new_ver = false;

View File

@@ -0,0 +1,188 @@
use super::crypto::*;
use crate::ext::io::*;
use crate::scripts::base::*;
use crate::types::*;
use crate::utils::encoding::decode_to_string;
use anyhow::Result;
use std::io::{Read, Seek, SeekFrom};
#[derive(Debug)]
pub struct EscudeBinArchiveBuilder {}
impl EscudeBinArchiveBuilder {
pub const fn new() -> Self {
EscudeBinArchiveBuilder {}
}
}
impl ScriptBuilder for EscudeBinArchiveBuilder {
fn default_encoding(&self) -> Encoding {
Encoding::Cp932
}
fn build_script(
&self,
data: Vec<u8>,
encoding: Encoding,
config: &ExtraConfig,
) -> Result<Box<dyn Script>> {
Ok(Box::new(EscudeBinArchive::new(data, encoding, config)?))
}
fn extensions(&self) -> &'static [&'static str] {
&["bin"]
}
fn script_type(&self) -> &'static ScriptType {
&ScriptType::EscudeArc
}
fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
if buf_len > 8 && buf.starts_with(b"ESC-ARC2") {
return Some(255);
}
None
}
}
#[derive(Debug)]
struct BinEntry {
name_offset: u32,
data_offset: u32,
length: u32,
}
struct Entry {
name: String,
data: Vec<u8>,
}
impl ArchiveContent for Entry {
fn name(&self) -> &str {
&self.name
}
fn data(&self) -> &[u8] {
&self.data
}
fn is_script(&self) -> bool {
self.data.starts_with(b"ESCR1_00")
}
}
#[derive(Debug)]
pub struct EscudeBinArchive {
reader: MemReader,
file_count: u32,
name_tbl_len: u32,
entries: Vec<BinEntry>,
encoding: Encoding,
}
impl EscudeBinArchive {
pub fn new(data: Vec<u8>, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
let mut reader = MemReader::new(data);
let mut header = [0u8; 8];
reader.read_exact(&mut header)?;
if &header != b"ESC-ARC2" {
return Err(anyhow::anyhow!("Invalid Escude binary script header"));
}
reader.seek(SeekFrom::Start(0xC))?;
let mut crypto_reader = CryptoReader::new(&mut reader)?;
let file_count = crypto_reader.read_u32()?;
let name_tbl_len = crypto_reader.read_u32()?;
let mut entries = Vec::with_capacity(file_count as usize);
for _ in 0..file_count {
let name_offset = crypto_reader.read_u32()?;
let data_offset = crypto_reader.read_u32()?;
let length = crypto_reader.read_u32()?;
entries.push(BinEntry {
name_offset,
data_offset,
length,
});
}
Ok(EscudeBinArchive {
reader,
file_count,
name_tbl_len,
entries,
encoding,
})
}
}
impl Script for EscudeBinArchive {
fn default_output_script_type(&self) -> OutputScriptType {
OutputScriptType::Json
}
fn default_format_type(&self) -> FormatOptions {
FormatOptions::None
}
fn is_archive(&self) -> bool {
true
}
fn iter_archive<'a>(
&'a self,
) -> Result<Box<dyn Iterator<Item = Result<Box<dyn ArchiveContent>>> + 'a>> {
let reader = self.reader.to_ref();
let encoding = self.encoding;
Ok(Box::new(EscudeBinArchiveIterator {
entries: self.entries.iter(),
reader,
encoding,
file_count: self.file_count,
}))
}
}
struct EscudeBinArchiveIterator<'a, T: Iterator<Item = &'a BinEntry>> {
entries: T,
reader: MemReaderRef<'a>,
encoding: Encoding,
file_count: u32,
}
impl<'a, T: Iterator<Item = &'a BinEntry>> Iterator for EscudeBinArchiveIterator<'a, T> {
type Item = Result<Box<dyn ArchiveContent>>;
fn next(&mut self) -> Option<Self::Item> {
let entry = match self.entries.next() {
Some(entry) => entry,
None => return None,
};
let name = match self
.reader
.peek_cstring_at(entry.name_offset as usize + self.file_count as usize * 12 + 0x14)
{
Ok(name) => name,
Err(e) => return Some(Err(e.into())),
};
let name = match decode_to_string(self.encoding, name.as_bytes()) {
Ok(name) => name,
Err(e) => return Some(Err(e.into())),
};
let mut data = match self
.reader
.peek_at_vec(entry.data_offset as usize, entry.length as usize)
{
Ok(data) => data,
Err(e) => return Some(Err(e.into())),
};
if data.starts_with(b"acp") {
let mut decoder = match super::lzw::LZWDecoder::new(&data) {
Ok(decoder) => decoder,
Err(e) => return Some(Err(anyhow::anyhow!("Failed to create LZW decoder: {}", e))),
};
data = match decoder.unpack() {
Ok(unpacked_data) => unpacked_data,
Err(e) => return Some(Err(anyhow::anyhow!("Failed to unpack LZW data: {}", e))),
};
}
Some(Ok(Box::new(Entry { name, data })))
}
}

View File

@@ -0,0 +1,54 @@
use crate::ext::io::*;
use anyhow::Result;
use std::io::{Read, Seek};
pub struct CryptoReader<T: Read + Seek> {
reader: T,
_key: u32,
max_pos: u32,
}
impl<T: Read + Seek> CryptoReader<T> {
pub fn new(mut reader: T) -> Result<Self> {
let _key = reader.peek_u32_at(0x8)?;
let mut s = CryptoReader {
reader,
_key,
max_pos: 0,
};
s.init()?;
Ok(s)
}
fn key(&mut self) -> u32 {
self._key ^= 0x65AC9365;
self._key ^= (((self._key >> 1) ^ self._key) >> 3) ^ (((self._key << 1) ^ self._key) << 3);
return self._key;
}
fn init(&mut self) -> Result<()> {
let _key = self._key;
self.max_pos = (self.reader.peek_u32_at(0xC)? ^ self.key()) * 12 + 0xC;
self._key = _key;
Ok(())
}
}
impl<T: Read + Seek> Read for CryptoReader<T> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let remaing = self.max_pos as usize + 0x8 - self.reader.stream_position()? as usize;
let count = buf.len().min(remaing);
let readed = self.reader.read(&mut buf[..count])?;
for i in 0..readed / 4 {
let val = u32::from_le_bytes(buf[i * 4..i * 4 + 4].try_into().map_err(|_| {
std::io::Error::new(
std::io::ErrorKind::InvalidData,
"Failed to convert slice to u32",
)
})?);
let decrypted = val ^ self.key();
buf[i * 4..i * 4 + 4].copy_from_slice(&decrypted.to_le_bytes());
}
Ok(readed)
}
}

98
src/scripts/escude/lzw.rs Normal file
View File

@@ -0,0 +1,98 @@
use crate::ext::io::*;
use anyhow::Result;
pub struct BitStream<'a> {
m_input: MemReaderRef<'a>,
m_bits: u32,
m_cached_bits: u32,
}
impl<'a> BitStream<'a> {
pub fn new(input: MemReaderRef<'a>) -> Self {
BitStream {
m_input: input,
m_bits: 0,
m_cached_bits: 0,
}
}
pub fn get_bits(&mut self, count: u32) -> Result<u32> {
while self.m_cached_bits < count {
let byte = self.m_input.read_u8()?;
self.m_bits = (self.m_bits << 8) | byte as u32;
self.m_cached_bits += 8;
}
let mask = (1 << count) - 1;
self.m_cached_bits -= count;
let result = (self.m_bits >> self.m_cached_bits) & mask;
Ok(result)
}
}
pub struct LZWDecoder<'a> {
m_input: BitStream<'a>,
m_output_size: u32,
}
impl<'a> LZWDecoder<'a> {
pub fn new(input: &'a [u8]) -> Result<Self> {
let mut input_reader = MemReaderRef::new(input);
let size = input_reader.peek_u32_be_at(0x4)?;
let m_input = BitStream::new(MemReaderRef::new(&input[0x8..]));
Ok(LZWDecoder {
m_input,
m_output_size: size,
})
}
pub fn unpack(&mut self) -> Result<Vec<u8>> {
let size = self.m_output_size as usize;
let mut output = Vec::with_capacity(size);
output.resize(size, 0);
let mut dict = Vec::with_capacity(0x8900);
dict.resize(0x8900, 0u32);
let mut token_width = 9;
let mut dict_pos = 0;
let mut dst = 0;
while dst < size {
let mut token = self.m_input.get_bits(token_width)?;
if token == 0x100 {
// End of stream
break;
} else if token == 0x101 {
token_width += 1;
if token_width > 24 {
return Err(anyhow::anyhow!("Token width exceeded maximum of 12 bits"));
}
} else if token == 0x102 {
token_width = 9;
dict_pos = 0;
} else {
if dict_pos > 0x8900 {
return Err(anyhow::anyhow!(
"Dictionary position exceeded maximum of 0x8900"
));
}
dict[dict_pos] = dst as u32;
dict_pos += 1;
if token < 0x100 {
output[dst] = token as u8;
dst += 1;
} else {
token -= 0x103;
if token >= dict_pos as u32 {
return Err(anyhow::anyhow!("Token out of bounds: {}", token));
}
let src = dict[token as usize];
let count =
(self.m_output_size - dst as u32).min(dict[token as usize + 1] - src + 1);
for i in 0..count {
output[dst + i as usize] = output[src as usize + i as usize];
}
dst += count as usize;
}
}
}
Ok(output)
}
}

View File

@@ -1 +1,4 @@
pub mod archive;
mod crypto;
mod lzw;
pub mod script;

View File

@@ -0,0 +1,210 @@
use crate::ext::io::*;
use crate::scripts::base::*;
use crate::types::*;
use crate::utils::encoding::{decode_to_string, encode_string};
use anyhow::Result;
use std::collections::HashMap;
use std::io::Read;
use unicode_segmentation::UnicodeSegmentation;
#[derive(Debug)]
pub struct EscudeBinScriptBuilder {}
impl EscudeBinScriptBuilder {
pub const fn new() -> Self {
EscudeBinScriptBuilder {}
}
}
impl ScriptBuilder for EscudeBinScriptBuilder {
fn default_encoding(&self) -> Encoding {
Encoding::Cp932
}
fn build_script(
&self,
data: Vec<u8>,
encoding: Encoding,
config: &ExtraConfig,
) -> Result<Box<dyn Script>> {
Ok(Box::new(EscudeBinScript::new(data, encoding, config)?))
}
fn extensions(&self) -> &'static [&'static str] {
&["bin"]
}
fn script_type(&self) -> &'static ScriptType {
&ScriptType::Escude
}
fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
if buf_len > 8 && buf.starts_with(b"ESCR1_00") {
return Some(255);
}
None
}
}
#[derive(Debug)]
pub struct EscudeBinScript {
offsets: Vec<u32>,
vms: Vec<u8>,
unk1: u32,
strings: Vec<String>,
}
impl EscudeBinScript {
pub fn new(data: Vec<u8>, encoding: Encoding, _config: &ExtraConfig) -> Result<Self> {
let mut reader = MemReader::new(data);
let mut magic = [0u8; 8];
reader.read_exact(&mut magic)?;
if &magic != b"ESCR1_00" {
return Err(anyhow::anyhow!(
"Invalid Escude binary script magic: {:?}",
magic
));
}
let string_count = reader.read_u32()?;
let mut offsets = Vec::with_capacity(string_count as usize);
for _ in 0..string_count {
offsets.push(reader.read_u32()?);
}
let vm_count = reader.read_u32()?;
let mut vms = Vec::with_capacity(vm_count as usize);
vms.resize(vm_count as usize, 0);
reader.read_exact(&mut vms)?;
let unk1 = reader.read_u32()?;
let mut strings = Vec::with_capacity(string_count as usize);
if encoding.is_jis() {
let replaces = StrReplacer::new()?;
for _ in 0..string_count {
let s = reader.read_cstring()?;
let s = replaces.replace(s.as_bytes())?;
strings.push(decode_to_string(encoding, &s)?);
}
} else {
for _ in 0..string_count {
let s = reader.read_cstring()?;
strings.push(decode_to_string(encoding, s.as_bytes())?);
}
}
Ok(EscudeBinScript {
offsets,
vms,
unk1,
strings,
})
}
}
impl Script for EscudeBinScript {
fn default_output_script_type(&self) -> OutputScriptType {
OutputScriptType::Json
}
fn default_format_type(&self) -> FormatOptions {
FormatOptions::None
}
fn extract_messages(&self) -> Result<Vec<Message>> {
Ok(self
.strings
.iter()
.map(|s| Message {
message: s.to_string(),
name: None,
})
.collect())
}
fn import_messages(
&self,
_messages: Vec<Message>,
_filename: &str,
_encoding: Encoding,
_replacement: Option<&ReplacementTable>,
) -> Result<()> {
Ok(())
}
fn is_archive(&self) -> bool {
false
}
}
struct StrReplacer {
pub replacements: HashMap<Vec<u8>, Vec<u8>>,
}
enum JisStr {
Single(u8),
Double(u8, u8),
}
impl StrReplacer {
pub fn new() -> Result<Self> {
let mut s = StrReplacer {
replacements: HashMap::new(),
};
s.add("!?。「」、・ヲァィゥェォャュョッーアイウエオカキクケコサシスセソタチツテトナニヌネノハヒフヘホマミムメモヤユヨラリルレロワン゙゚", "!? 。「」、…をぁぃぅぇぉゃゅょっーあいうえおかきくけこさしすせそたちつてとなにぬねのはひふへほまみむめもやゆよらりるれろわん゛゜")?;
Ok(s)
}
fn add(&mut self, from: &str, to: &str) -> Result<()> {
let encoding = Encoding::Cp932; // Default encoding, can be changed as needed
let froms = UnicodeSegmentation::graphemes(from, true);
let tos = UnicodeSegmentation::graphemes(to, true);
for (from, to) in froms.zip(tos) {
let from_bytes = if from == "" {
vec![0xa0]
} else {
encode_string(encoding, from, true)?
};
let to_bytes = encode_string(encoding, to, true)?;
self.replacements.insert(from_bytes, to_bytes);
}
Ok(())
}
pub fn replace(&self, input: &[u8]) -> Result<Vec<u8>> {
let mut result = Vec::new();
let mut reader = MemReaderRef::new(input);
while let Ok(byte) = reader.read_u8() {
if byte < 0x80 || (byte >= 0xa0 && byte <= 0xdf) {
result.push(JisStr::Single(byte));
} else if (byte >= 0x81 && byte <= 0x9f) || (byte >= 0xe0 && byte <= 0xef) {
let next_byte = reader.read_u8()?;
if next_byte < 0x40 || next_byte > 0xfc {
return Err(anyhow::anyhow!("Invalid JIS encoding sequence"));
}
result.push(JisStr::Double(byte, next_byte));
} else {
return Err(anyhow::anyhow!("Invalid byte in JIS encoding: {}", byte));
}
}
let mut output = Vec::new();
for item in result {
match item {
JisStr::Single(byte) => {
let vec = vec![byte];
if let Some(replacement) = self.replacements.get(&vec) {
output.extend_from_slice(replacement);
} else {
output.push(byte);
}
}
JisStr::Double(byte1, byte2) => {
let key = vec![byte1, byte2];
if let Some(replacement) = self.replacements.get(&key) {
output.extend_from_slice(replacement);
} else {
output.push(byte1);
output.push(byte2);
}
}
}
}
Ok(output)
}
}

View File

@@ -9,6 +9,8 @@ lazy_static::lazy_static! {
pub static ref BUILDER: Vec<Box<dyn ScriptBuilder + Sync + Send>> = vec![
Box::new(circus::script::CircusMesScriptBuilder::new()),
Box::new(bgi::script::BGIScriptBuilder::new()),
Box::new(escude::archive::EscudeBinArchiveBuilder::new()),
Box::new(escude::script::EscudeBinScriptBuilder::new()),
];
pub static ref ALL_EXTS: Vec<String> =
BUILDER.iter().flat_map(|b| b.extensions()).map(|s| s.to_string()).collect();