diff --git a/Cargo.toml b/Cargo.toml index 704177e..21a2f03 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -118,7 +118,7 @@ will-plus = ["utils-str"] will-plus-img = ["will-plus", "image"] yaneurao = [] yaneurao-itufuru = ["yaneurao", "utils-xored-stream"] -yuris = ["dep:hex", "utils-xored-stream"] +yuris = ["dep:hex", "utils-serde-base64bytes", "utils-xored-stream"] yuris-img = ["yuris", "image", "qoi", "webp"] # basic feature image = ["dep:png"] diff --git a/README.md b/README.md index c9270c2..91951c2 100644 --- a/README.md +++ b/README.md @@ -269,7 +269,7 @@ msg-tool create -t | `yuris-yscm` | `yuris` | Yu-Ris YSCM(opcodes metadata) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ | | | `yuris-yser` | `yuris` | Yu-Ris YSER(error message) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | | | `yuris-yscfg` | `yuris` | Yu-Ris YSCFG(config) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | | -| `yuris-ystb` | `yuris` | Yu-Ris YSTB(compiled script) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ | | +| `yuris-ystb` | `yuris` | Yu-Ris YSTB(compiled script) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ❌ | | | `yuris-txt` | `yuris` | Yu-Ris scenario text file (.txt) | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | | | Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks | diff --git a/src/args.rs b/src/args.rs index 2e0066a..b621d2f 100644 --- a/src/args.rs +++ b/src/args.rs @@ -760,6 +760,10 @@ pub struct Arg { #[arg(long, global = true)] /// Path to the ysc.ybn file pub yuris_ysc_path: Option, + #[cfg(feature = "yuris")] + #[arg(long, global = true)] + /// Disasm Yu-RIS YSTB (.ybn) file + pub yuris_ystb_disasm: bool, #[command(subcommand)] /// Command pub command: Command, diff --git a/src/ext/io.rs b/src/ext/io.rs index 34da75f..bb54557 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -1443,6 +1443,16 @@ pub trait WriteAt { fn write_cstring_at(&mut self, offset: u64, value: &CString) -> Result<()> { self.write_all_at(offset, value.as_bytes_with_nul()) } + + /// Write a struct at a specific offset. + fn write_struct_at( + &mut self, + offset: u64, + value: &V, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result<()>; } impl WriteAt for T { @@ -1461,6 +1471,23 @@ impl WriteAt for T { self.seek(SeekFrom::Start(current_pos))?; Ok(()) } + + fn write_struct_at( + &mut self, + offset: u64, + value: &V, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result<()> { + let current_pos = self.stream_position()?; + self.seek(SeekFrom::Start(offset as u64))?; + value + .pack(self, big, encoding, info) + .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e))?; + self.seek(SeekFrom::Start(current_pos))?; + Ok(()) + } } /// A trait to help to seek in a stream. diff --git a/src/main.rs b/src/main.rs index a91dfb3..4a92d3b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3415,6 +3415,8 @@ fn main() { xp3_cxdec_path_hash: arg.xp3_cxdec_path_hash, #[cfg(feature = "yuris")] yuris_ysc_path: arg.yuris_ysc_path.clone(), + #[cfg(feature = "yuris")] + yuris_ystb_disasm: arg.yuris_ystb_disasm, }); match &arg.command { args::Command::Export { input, output } => { diff --git a/src/scripts/yuris/ystb.rs b/src/scripts/yuris/ystb.rs index a05663f..f34f89d 100644 --- a/src/scripts/yuris/ystb.rs +++ b/src/scripts/yuris/ystb.rs @@ -4,21 +4,28 @@ use crate::ext::io::*; use crate::scripts::base::*; use crate::types::*; use crate::utils::encoding::*; +use crate::utils::serde_base64bytes::*; use crate::utils::struct_pack::*; use crate::utils::xored_stream::*; use anyhow::Result; use msg_tool_macro::*; +use serde::{Deserialize, Serialize}; use std::any::Any; use std::io::{Read, Seek, SeekFrom, Write}; use std::ops::{Deref, DerefMut}; -#[derive(Clone, Debug, StructUnpack, StructPack)] +#[derive(Clone, Debug, StructUnpack, StructPack, Deserialize, Serialize)] struct YSTBHeader { version: u32, + #[serde(skip)] inst_entry_count: u32, + #[serde(skip)] inst_index_size: u32, + #[serde(skip)] args_index_size: u32, + #[serde(skip)] args_data_size: u32, + #[serde(skip)] line_numbers_size: u32, reserve0: u32, } @@ -34,10 +41,12 @@ struct YSTBHeaderV2 { reserved2: u32, } +#[derive(Deserialize, Serialize)] struct YSTBData { + #[serde(flatten)] header: YSTBHeader, insts: Vec, - line_numbers: Vec, + line_numbers: Base64Bytes, } impl std::fmt::Debug for YSTBData { @@ -45,7 +54,7 @@ impl std::fmt::Debug for YSTBData { f.debug_struct("YSTBData") .field("header", &self.header) .field("insts", &self.insts) - .field("line_numbers", &hex::encode(&self.line_numbers)) + .field("line_numbers", &hex::encode(&self.line_numbers.bytes)) .finish() } } @@ -88,19 +97,22 @@ impl StructUnpack for YSTBData { Ok(Self { header, insts, - line_numbers, + line_numbers: line_numbers.into(), }) } } -#[derive(Debug, StructUnpack, StructPack)] +#[derive(Debug, StructUnpack, StructPack, Deserialize, Serialize)] struct YSTBInstBase { opcode: u8, + #[serde(skip)] arg_count: u8, unk: u16, } +#[derive(Deserialize, Serialize)] struct YSTBInst { + #[serde(flatten)] base: YSTBInstBase, args: Vec, } @@ -129,10 +141,11 @@ impl std::fmt::Debug for YSTBInst { } } -#[derive(Debug, StructUnpack, StructPack)] +#[derive(Clone, Debug, StructUnpack, StructPack, Deserialize, Serialize)] struct YSTBArgBase { id: u16, typ: u16, + #[serde(skip)] size: u32, } @@ -142,6 +155,88 @@ struct YSTBArg { encoding: Encoding, } +#[derive(Deserialize, Serialize)] +#[serde(tag = "t")] +enum YSTBArgDat { + Raw { data: Base64Bytes }, + MString { s: String }, +} + +impl TryFrom for YSTBArg { + type Error = anyhow::Error; + fn try_from(value: YSTBArgTmp) -> Result { + let data = match value.data { + YSTBArgDat::Raw { data } => data.bytes, + YSTBArgDat::MString { s } => { + let mut m = MemWriter::new(); + m.write_u8(b'M')?; + let d = encode_string(value.encoding, &s, true)?; + m.write_u16(d.len() as u16)?; + m.write_all(&d)?; + m.into_inner() + } + }; + Ok(Self { + base: value.base, + data, + encoding: value.encoding, + }) + } +} + +impl<'a> TryFrom<&'a YSTBArg> for YSTBArgTmp { + type Error = anyhow::Error; + fn try_from(value: &'a YSTBArg) -> Result { + if value.data.len() >= 5 && value.data.starts_with(b"M") { + let len = u16::from_le_bytes([value.data[1], value.data[2]]); + if len as usize == value.data.len() - 3 { + if let Ok(s) = decode_to_string(value.encoding, &value.data[3..], true) { + return Ok(Self { + base: value.base.clone(), + data: YSTBArgDat::MString { s }, + encoding: value.encoding, + }); + } + } + } + Ok(Self { + base: value.base.clone(), + data: YSTBArgDat::Raw { + data: value.data.clone().into(), + }, + encoding: value.encoding, + }) + } +} + +#[derive(Deserialize, Serialize)] +struct YSTBArgTmp { + #[serde(flatten)] + base: YSTBArgBase, + data: YSTBArgDat, + encoding: Encoding, +} + +impl<'de> Deserialize<'de> for YSTBArg { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let tmp = YSTBArgTmp::deserialize(deserializer)?; + tmp.try_into().map_err(serde::de::Error::custom) + } +} + +impl Serialize for YSTBArg { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let tmp: YSTBArgTmp = self.try_into().map_err(serde::ser::Error::custom)?; + tmp.serialize(serializer) + } +} + struct YSTBArgData<'a>(&'a [u8], Encoding); impl<'a> std::fmt::Debug for YSTBArgData<'a> { @@ -286,8 +381,9 @@ impl ScriptBuilder for YSTBBuilder { pub struct YSTB { data: YSTBData, com: YSCMData, - #[allow(unused)] xor_key: Option, + disasm: bool, + custom_yaml: bool, } impl YSTB { @@ -340,7 +436,13 @@ impl YSTB { let mut reader = MemReader::new(yscm); reader.pos = 4; let com = YSCMData::unpack(&mut reader, false, encoding, &None)?; - Ok(Self { data, com, xor_key }) + Ok(Self { + data, + com, + xor_key, + disasm: config.yuris_ystb_disasm, + custom_yaml: config.custom_yaml, + }) } fn get_xor_key(reader: &mut T) -> Result { @@ -370,7 +472,7 @@ impl YSTB { ) -> Result<()> { let key = xor_key.to_le_bytes(); reader.seek(SeekFrom::Start(4))?; - writer.write_all(b"YSCM")?; + writer.write_all(b"YSTB")?; let version = reader.peek_u32()?; if matches!(version, 201..300) { let header: YSTBHeaderV2 = reader.read_struct(false, Encoding::Cp932, &None)?; @@ -435,10 +537,27 @@ impl Script for YSTB { } fn custom_output_extension(&self) -> &'static str { - "txt" + if self.disasm { + "txt" + } else if self.custom_yaml { + "yaml" + } else { + "json" + } } fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> { + if !self.disasm { + let s = if self.custom_yaml { + serde_yaml_ng::to_string(&self.data)? + } else { + serde_json::to_string_pretty(&self.data)? + }; + let mut f = std::fs::File::create(filename)?; + let encoded = encode_string(encoding, &s, true)?; + f.write_all(&encoded)?; + return Ok(()); + } let mut file = MemWriter::new(); let mut indent = String::new(); for code in self.data.insts.iter() { @@ -503,4 +622,71 @@ impl Script for YSTB { } Ok(()) } + + fn custom_import<'a>( + &'a self, + custom_filename: &'a str, + mut file: Box, + encoding: Encoding, + output_encoding: Encoding, + ) -> Result<()> { + if self.disasm { + anyhow::bail!("Import is not supported for disasm mode."); + } + let mut f = MemWriter::new(); + let data = crate::utils::files::read_file(custom_filename)?; + let data = decode_to_string(output_encoding, &data, true)?; + let mut data: YSTBData = if self.custom_yaml { + serde_yaml_ng::from_str(&data)? + } else { + serde_json::from_str(&data)? + }; + f.write_all(b"YSTB")?; + data.header.line_numbers_size = data.line_numbers.len() as u32; + data.header.inst_entry_count = data.insts.len() as u32; + data.header.inst_index_size = data.header.inst_entry_count * 4; + data.header.pack(&mut f, false, encoding, &None)?; + for i in data.insts.iter_mut() { + i.base.arg_count = i.args.len() as u8; + i.base.pack(&mut f, false, encoding, &None)?; + } + let arg_count = data.insts.iter().fold(0, |c, i| c + i.args.len()); + data.header.args_index_size = arg_count as u32 * 0xC; + let mut cpos = f.pos as u64; + f.pos += data.header.args_index_size as usize; + let bpos = f.pos as u32; + for i in data.insts.iter_mut() { + let meta = + self.com.opcodes.get(i.opcode as usize).ok_or_else(|| { + anyhow::anyhow!("Failed to find op {:x}'s metadata", i.opcode) + })?; + for arg in i.args.iter_mut() { + arg.base.size = arg.data.len() as u32; + f.write_struct_at(cpos, &arg.base, false, encoding, &None)?; + cpos += 8; + if arg.base.size == 0 + || (meta.name == "RETURNCODE" && arg.base.size == 1 && arg.data[0] == b'M') + { + f.write_u32_at(cpos, 0)?; + cpos += 4; + continue; + } + let offset = f.pos as u32 - bpos; + f.write_u32_at(cpos, offset)?; + cpos += 4; + f.write_all(&arg.data)?; + } + } + data.header.args_data_size = f.pos as u32 - bpos; + f.write_all(&data.line_numbers)?; + f.pos = 4; + data.header.pack(&mut f, false, encoding, &None)?; + if let Some(xor) = self.xor_key { + let mut r = MemReader::new(f.into_inner()); + f = MemWriter::new(); + Self::xor(&mut r, &mut f, xor)?; + } + file.write_all(&f.data)?; + Ok(()) + } } diff --git a/src/types.rs b/src/types.rs index 8139c73..d71ac9b 100644 --- a/src/types.rs +++ b/src/types.rs @@ -4,7 +4,7 @@ use serde::{Deserialize, Serialize}; use std::collections::HashMap; #[derive(Copy, Clone, Serialize, Deserialize, Debug)] -#[serde(untagged, rename_all = "camelCase")] +#[serde(rename_all = "camelCase")] /// Text Encoding pub enum Encoding { /// Automatically detect encoding @@ -666,6 +666,9 @@ pub struct ExtraConfig { #[cfg(feature = "yuris")] /// Path to the ysc.ybn file pub yuris_ysc_path: Option, + #[cfg(feature = "yuris")] + /// Disasm Yu-RIS YSTB (.ybn) file + pub yuris_ystb_disasm: bool, } #[cfg(feature = "artemis")] diff --git a/src/utils/serde_base64bytes.rs b/src/utils/serde_base64bytes.rs index d1f8557..30ea1ce 100644 --- a/src/utils/serde_base64bytes.rs +++ b/src/utils/serde_base64bytes.rs @@ -21,6 +21,12 @@ impl DerefMut for Base64Bytes { } } +impl From> for Base64Bytes { + fn from(value: Vec) -> Self { + Self { bytes: value } + } +} + impl Serialize for Base64Bytes { fn serialize(&self, serializer: S) -> Result where