mirror of
https://github.com/lifegpc/msg-tool.git
synced 2026-07-08 01:31:53 +08:00
Compare commits
2 Commits
3469f6babb
...
2e306df6da
| Author | SHA1 | Date | |
|---|---|---|---|
| 2e306df6da | |||
| 1f205f2cd0 |
@@ -159,6 +159,7 @@ msg-tool create -t <script-type> <input> <output>
|
||||
| `kirikiri-simple-crypt`/`kr-simple-crypt` | `kirikiri` | Kirikiri Simple Crypt Text File | ❌ | ❌ | ✔️ | ❌ | ❌ | |
|
||||
| `kirikiri-mdf`/`kr-mdf` | `kirikiri` | Kirikiri Zlib-Compressed File | ❌ | ❌ | ✔️ | ❌ | ❌ | |
|
||||
| `kirikiri-tjs-ns0`/`kr-tjs-ns0` | `kirikiri` | Kirikiri TJS NS0 binary encoded script | ❌ | ❌ | ✔️ | ❌ | ❌ | |
|
||||
| `kirikiri-tjs2`/`kr-tjs2` | `kirikiri` | Kirikiri compiled TJS2 script | ✔️ | ✔️ | ✔️ | ✔️ | ❌ | |
|
||||
|
||||
| Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
//!Extensions for IO operations.
|
||||
use crate::types::Encoding;
|
||||
use crate::utils::encoding::decode_to_string;
|
||||
use crate::{types::Encoding, utils::struct_pack::StructUnpack};
|
||||
use crate::utils::struct_pack::{StructPack, StructUnpack};
|
||||
use std::ffi::CString;
|
||||
use std::io::*;
|
||||
use std::sync::Mutex;
|
||||
@@ -971,6 +972,13 @@ pub trait WriteExt {
|
||||
|
||||
/// Writes a C-style string (null-terminated) to the writer.
|
||||
fn write_cstring(&mut self, value: &CString) -> Result<()>;
|
||||
/// Write a struct to the writer.
|
||||
fn write_struct<T: StructPack>(
|
||||
&mut self,
|
||||
value: &T,
|
||||
big: bool,
|
||||
encoding: Encoding,
|
||||
) -> Result<()>;
|
||||
}
|
||||
|
||||
impl<T: Write> WriteExt for T {
|
||||
@@ -1032,6 +1040,17 @@ impl<T: Write> WriteExt for T {
|
||||
fn write_cstring(&mut self, value: &CString) -> Result<()> {
|
||||
self.write_all(value.as_bytes_with_nul())
|
||||
}
|
||||
|
||||
fn write_struct<V: StructPack>(
|
||||
&mut self,
|
||||
value: &V,
|
||||
big: bool,
|
||||
encoding: Encoding,
|
||||
) -> Result<()> {
|
||||
value
|
||||
.pack(self, big, encoding)
|
||||
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait to help to write data to a writer at a specific offset.
|
||||
@@ -1144,6 +1163,9 @@ impl<T: Write + Seek> WriteAt for T {
|
||||
pub trait SeekExt {
|
||||
/// Returns the length of the stream.
|
||||
fn stream_length(&mut self) -> Result<u64>;
|
||||
/// Aligns the current position to the given alignment.
|
||||
/// Returns the new position after alignment.
|
||||
fn align(&mut self, align: u64) -> Result<u64>;
|
||||
}
|
||||
|
||||
impl<T: Seek> SeekExt for T {
|
||||
@@ -1153,6 +1175,15 @@ impl<T: Seek> SeekExt for T {
|
||||
self.seek(SeekFrom::Start(current_pos))?;
|
||||
Ok(length)
|
||||
}
|
||||
|
||||
fn align(&mut self, align: u64) -> Result<u64> {
|
||||
let current_pos = self.stream_position()?;
|
||||
let aligned_pos = (current_pos + align - 1) & !(align - 1);
|
||||
if aligned_pos != current_pos {
|
||||
self.seek(SeekFrom::Start(aligned_pos))?;
|
||||
}
|
||||
Ok(aligned_pos)
|
||||
}
|
||||
}
|
||||
|
||||
/// A memory reader that can read data from a vector of bytes.
|
||||
@@ -1457,6 +1488,9 @@ impl Write for MemWriter {
|
||||
}
|
||||
|
||||
impl Seek for MemWriter {
|
||||
/// Seeks to a new position in the writer.
|
||||
/// If the new position is beyond the current length of the data, the data is resized when writing.
|
||||
/// (This means that seeking beyond the end does not immediately resize the data.)
|
||||
fn seek(&mut self, pos: SeekFrom) -> Result<u64> {
|
||||
match pos {
|
||||
SeekFrom::Start(offset) => {
|
||||
|
||||
@@ -5,6 +5,7 @@ pub mod ks;
|
||||
pub mod mdf;
|
||||
pub mod scn;
|
||||
pub mod simple_crypt;
|
||||
pub mod tjs2;
|
||||
pub mod tjs_ns0;
|
||||
use std::collections::HashMap;
|
||||
use std::sync::Arc;
|
||||
|
||||
306
src/scripts/kirikiri/tjs2.rs
Normal file
306
src/scripts/kirikiri/tjs2.rs
Normal file
@@ -0,0 +1,306 @@
|
||||
//! Kirikiri compiled TJS2 script
|
||||
use crate::ext::io::*;
|
||||
use crate::scripts::base::*;
|
||||
use crate::types::*;
|
||||
use crate::utils::encoding::*;
|
||||
use crate::utils::struct_pack::*;
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io::{Read, Seek, Write};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Kirikiri TJS2 Script Builder
|
||||
pub struct Tjs2Builder {}
|
||||
|
||||
impl Tjs2Builder {
|
||||
/// Creates a new instance of `Tjs2Builder`
|
||||
pub fn new() -> Self {
|
||||
Self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ScriptBuilder for Tjs2Builder {
|
||||
fn default_encoding(&self) -> Encoding {
|
||||
Encoding::Utf16LE
|
||||
}
|
||||
|
||||
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(Tjs2::new(buf, encoding, config)?))
|
||||
}
|
||||
|
||||
fn extensions(&self) -> &'static [&'static str] {
|
||||
&["tjs"]
|
||||
}
|
||||
|
||||
fn script_type(&self) -> &'static ScriptType {
|
||||
&ScriptType::KirikiriTjs2
|
||||
}
|
||||
|
||||
fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option<u8> {
|
||||
// TJS2 tag 100 version
|
||||
if buf_len >= 8 && buf.starts_with(b"TJS2100\0") {
|
||||
return Some(40);
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
struct DataArea {
|
||||
byte_array: Vec<u8>,
|
||||
short_array: Vec<i16>,
|
||||
long_array: Vec<i32>,
|
||||
longlong_array: Vec<i64>,
|
||||
double_array: Vec<f64>,
|
||||
string_array: Vec<String>,
|
||||
octet_array: Vec<Vec<u8>>,
|
||||
}
|
||||
|
||||
impl StructUnpack for DataArea {
|
||||
fn unpack<R: Read + Seek>(reader: &mut R, big: bool, encoding: Encoding) -> Result<Self> {
|
||||
reader.align(4)?;
|
||||
let start_loc = reader.stream_position()?;
|
||||
let mut data_tag = [0; 4];
|
||||
reader.read_exact(&mut data_tag)?;
|
||||
if &data_tag != b"DATA" {
|
||||
return Err(anyhow::anyhow!("Invalid DATA tag"));
|
||||
}
|
||||
let data_size = u32::unpack(reader, big, encoding)?;
|
||||
let count = u32::unpack(reader, big, encoding)? as usize;
|
||||
let byte_array = reader.read_exact_vec(count)?;
|
||||
reader.align(4)?;
|
||||
let short_count = u32::unpack(reader, big, encoding)? as usize;
|
||||
let short_array = reader.read_struct_vec(short_count, big, encoding)?;
|
||||
reader.align(4)?;
|
||||
let long_count = u32::unpack(reader, big, encoding)? as usize;
|
||||
let long_array = reader.read_struct_vec(long_count, big, encoding)?;
|
||||
let longlong_count = u32::unpack(reader, big, encoding)? as usize;
|
||||
let longlong_array = reader.read_struct_vec(longlong_count, big, encoding)?;
|
||||
let double_count = u32::unpack(reader, big, encoding)? as usize;
|
||||
let double_array = reader.read_struct_vec(double_count, big, encoding)?;
|
||||
let str_count = u32::unpack(reader, big, encoding)? as usize;
|
||||
let mut string_array = Vec::with_capacity(str_count);
|
||||
for _ in 0..str_count {
|
||||
let str_len = u32::unpack(reader, big, encoding)? as usize;
|
||||
let str_bytes = reader.read_exact_vec(if encoding.is_utf16le() {
|
||||
str_len * 2
|
||||
} else {
|
||||
str_len
|
||||
})?;
|
||||
let s = decode_to_string(encoding, &str_bytes, true)?;
|
||||
reader.align(4)?;
|
||||
string_array.push(s);
|
||||
}
|
||||
let octet_count = u32::unpack(reader, big, encoding)? as usize;
|
||||
let mut octet_array = Vec::with_capacity(octet_count);
|
||||
for _ in 0..octet_count {
|
||||
let octet_len = u32::unpack(reader, big, encoding)? as usize;
|
||||
let octet_bytes = reader.read_exact_vec(octet_len)?;
|
||||
reader.align(4)?;
|
||||
octet_array.push(octet_bytes);
|
||||
}
|
||||
let end_loc = reader.stream_position()?;
|
||||
if end_loc - start_loc != data_size as u64 {
|
||||
return Err(anyhow::anyhow!(
|
||||
"DATA size mismatch: expected {}, got {}",
|
||||
data_size,
|
||||
end_loc - start_loc
|
||||
));
|
||||
}
|
||||
Ok(DataArea {
|
||||
byte_array,
|
||||
short_array,
|
||||
long_array,
|
||||
longlong_array,
|
||||
double_array,
|
||||
string_array,
|
||||
octet_array,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl StructPack for DataArea {
|
||||
fn pack<W: Write>(&self, writer: &mut W, big: bool, encoding: Encoding) -> Result<()> {
|
||||
writer.write_all(b"DATA")?;
|
||||
let mut tmp = MemWriter::new();
|
||||
tmp.write_struct(&(self.byte_array.len() as u32), big, encoding)?;
|
||||
tmp.write_all(&self.byte_array)?;
|
||||
tmp.align(4)?;
|
||||
tmp.write_struct(&(self.short_array.len() as u32), big, encoding)?;
|
||||
for v in &self.short_array {
|
||||
tmp.write_struct(v, big, encoding)?;
|
||||
}
|
||||
tmp.align(4)?;
|
||||
tmp.write_struct(&(self.long_array.len() as u32), big, encoding)?;
|
||||
for v in &self.long_array {
|
||||
tmp.write_struct(v, big, encoding)?;
|
||||
}
|
||||
tmp.write_struct(&(self.longlong_array.len() as u32), big, encoding)?;
|
||||
for v in &self.longlong_array {
|
||||
tmp.write_struct(v, big, encoding)?;
|
||||
}
|
||||
tmp.write_struct(&(self.double_array.len() as u32), big, encoding)?;
|
||||
for v in &self.double_array {
|
||||
tmp.write_struct(v, big, encoding)?;
|
||||
}
|
||||
tmp.write_struct(&(self.string_array.len() as u32), big, encoding)?;
|
||||
for s in &self.string_array {
|
||||
let encoded = encode_string(encoding, s, false)?;
|
||||
let str_len = if encoding.is_utf16le() {
|
||||
encoded.len() / 2
|
||||
} else {
|
||||
encoded.len()
|
||||
};
|
||||
tmp.write_struct(&(str_len as u32), big, encoding)?;
|
||||
tmp.write_all(&encoded)?;
|
||||
tmp.align(4)?;
|
||||
}
|
||||
tmp.write_struct(&(self.octet_array.len() as u32), big, encoding)?;
|
||||
for o in &self.octet_array {
|
||||
tmp.write_struct(&(o.len() as u32), big, encoding)?;
|
||||
tmp.write_all(o)?;
|
||||
tmp.align(4)?;
|
||||
}
|
||||
// make sure final size is aligned to 4 bytes
|
||||
tmp.data.resize(tmp.pos, 0);
|
||||
let data = tmp.into_inner();
|
||||
writer.write_struct(&(data.len() as u32 + 8), big, encoding)?;
|
||||
writer.write_all(&data)?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Kirikiri TJS2 Script
|
||||
#[derive(Debug)]
|
||||
pub struct Tjs2 {
|
||||
data_area: DataArea,
|
||||
remaing: Vec<u8>,
|
||||
custom_yaml: bool,
|
||||
}
|
||||
|
||||
impl Tjs2 {
|
||||
/// Creates a new `Tjs2` script from the given buffer
|
||||
///
|
||||
/// * `buf` - The buffer containing the TJS2 data
|
||||
/// * `encoding` - The encoding to use for strings
|
||||
/// * `config` - Extra configuration options
|
||||
pub fn new(buf: Vec<u8>, encoding: Encoding, config: &ExtraConfig) -> Result<Self> {
|
||||
let mut reader = MemReader::new(buf);
|
||||
let mut header = [0u8; 8];
|
||||
reader.read_exact(&mut header)?;
|
||||
if &header != b"TJS2100\0" {
|
||||
return Err(anyhow::anyhow!("Invalid TJS2 header: {:?}", &header));
|
||||
}
|
||||
let _file_size = reader.read_u32()?;
|
||||
let data_area = DataArea::unpack(&mut reader, false, encoding)?;
|
||||
let mut remaing = Vec::new();
|
||||
reader.read_to_end(&mut remaing)?;
|
||||
Ok(Self {
|
||||
data_area,
|
||||
remaing,
|
||||
custom_yaml: config.custom_yaml,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Script for Tjs2 {
|
||||
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 s in self.data_area.string_array.iter() {
|
||||
messages.push(Message {
|
||||
name: None,
|
||||
message: s.clone(),
|
||||
});
|
||||
}
|
||||
Ok(messages)
|
||||
}
|
||||
|
||||
fn import_messages<'a>(
|
||||
&'a self,
|
||||
messages: Vec<Message>,
|
||||
mut file: Box<dyn WriteSeek + 'a>,
|
||||
_filename: &str,
|
||||
encoding: Encoding,
|
||||
replacement: Option<&'a ReplacementTable>,
|
||||
) -> Result<()> {
|
||||
let mut data_area = self.data_area.clone();
|
||||
data_area.string_array = messages
|
||||
.iter()
|
||||
.map(|m| {
|
||||
let mut s = m.message.clone();
|
||||
if let Some(table) = replacement {
|
||||
for (from, to) in &table.map {
|
||||
s = s.replace(from, to);
|
||||
}
|
||||
}
|
||||
s
|
||||
})
|
||||
.collect();
|
||||
file.write_all(b"TJS2100\0")?;
|
||||
file.write_u32(0)?; // placeholder for file size
|
||||
data_area.pack(&mut file, false, encoding)?;
|
||||
file.write_all(&self.remaing)?;
|
||||
let file_size = file.stream_length()?;
|
||||
file.write_u32_at(8, file_size as u32)?; // write actual file size
|
||||
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_area)?
|
||||
} else {
|
||||
serde_json::to_string_pretty(&self.data_area)?
|
||||
};
|
||||
let encoded = encode_string(encoding, &s, false)?;
|
||||
let mut file = crate::utils::files::write_file(filename)?;
|
||||
file.write_all(&encoded)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn custom_import<'a>(
|
||||
&'a self,
|
||||
custom_filename: &'a str,
|
||||
mut file: Box<dyn WriteSeek + 'a>,
|
||||
encoding: Encoding,
|
||||
output_encoding: Encoding,
|
||||
) -> Result<()> {
|
||||
let data = crate::utils::files::read_file(custom_filename)?;
|
||||
let s = decode_to_string(output_encoding, &data, true)?;
|
||||
let data_area: DataArea = if self.custom_yaml {
|
||||
serde_yaml_ng::from_str(&s)?
|
||||
} else {
|
||||
serde_json::from_str(&s)?
|
||||
};
|
||||
file.write_all(b"TJS2100\0")?;
|
||||
file.write_u32(0)?; // placeholder for file size
|
||||
data_area.pack(&mut file, false, encoding)?;
|
||||
file.write_all(&self.remaing)?;
|
||||
let file_size = file.stream_length()?;
|
||||
file.write_u32_at(8, file_size as u32)?; // write actual file size
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -116,6 +116,8 @@ lazy_static::lazy_static! {
|
||||
Box::new(artemis::panmimisoft::txt::TxtBuilder::new()),
|
||||
#[cfg(feature = "kirikiri")]
|
||||
Box::new(kirikiri::tjs_ns0::TjsNs0Builder::new()),
|
||||
#[cfg(feature = "kirikiri")]
|
||||
Box::new(kirikiri::tjs2::Tjs2Builder::new()),
|
||||
];
|
||||
/// A list of all script extensions.
|
||||
pub static ref ALL_EXTS: Vec<String> =
|
||||
|
||||
@@ -518,6 +518,10 @@ pub enum ScriptType {
|
||||
/// Kirikiri MDF (zlib compressed) file
|
||||
KirikiriMdf,
|
||||
#[cfg(feature = "kirikiri")]
|
||||
#[value(alias("kr-tjs2"))]
|
||||
/// Kirikiri compiled TJS2 script
|
||||
KirikiriTjs2,
|
||||
#[cfg(feature = "kirikiri")]
|
||||
#[value(alias("kr-tjs-ns0"))]
|
||||
/// Kirikiri TJS NS0 binary encoded script
|
||||
KirikiriTjsNs0,
|
||||
|
||||
Reference in New Issue
Block a user