From 8c767fd9e92a83c34149e29e713cdd43aab66d86 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Tue, 2 Jun 2026 14:39:51 +0800 Subject: [PATCH] Add support for Yu-Ris YSVR(Variables) file (.ybn) --- README.md | 1 + src/scripts/mod.rs | 2 + src/scripts/yuris/mod.rs | 1 + src/scripts/yuris/ysvr.rs | 382 ++++++++++++++++++++++++++++++++++++++ src/types.rs | 3 + 5 files changed, 389 insertions(+) create mode 100644 src/scripts/yuris/ysvr.rs diff --git a/README.md b/README.md index 206b66d..fd14171 100644 --- a/README.md +++ b/README.md @@ -273,6 +273,7 @@ msg-tool create -t | `yuris-txt` | `yuris` | Yu-Ris scenario text file (.txt) | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | | | `yuris-ystl` | `yuris` | Yu-Ris YSTL(file list) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | | | `yuris-yslb` | `yuris` | Yu-Ris YSLB(labels) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | | +| `yuris-ysvr` | `yuris` | Yu-Ris YSVR(Variables) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | | | Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks | |---|---|---|---|---|---|---|---|---| diff --git a/src/scripts/mod.rs b/src/scripts/mod.rs index 035e13d..9a14c16 100644 --- a/src/scripts/mod.rs +++ b/src/scripts/mod.rs @@ -192,6 +192,8 @@ lazy_static::lazy_static! { Box::new(yuris::ystl::YSTLBuilder::new()), #[cfg(feature = "yuris")] Box::new(yuris::yslb::YSLBBuilder::new()), + #[cfg(feature = "yuris")] + Box::new(yuris::ysvr::YSVRBuilder::new()), ]; /// A list of all script extensions. pub static ref ALL_EXTS: Vec = diff --git a/src/scripts/yuris/mod.rs b/src/scripts/yuris/mod.rs index cc3ba86..4b1f5b4 100644 --- a/src/scripts/yuris/mod.rs +++ b/src/scripts/yuris/mod.rs @@ -9,3 +9,4 @@ pub mod yser; pub mod yslb; pub mod ystb; pub mod ystl; +pub mod ysvr; diff --git a/src/scripts/yuris/ysvr.rs b/src/scripts/yuris/ysvr.rs new file mode 100644 index 0000000..8852d00 --- /dev/null +++ b/src/scripts/yuris/ysvr.rs @@ -0,0 +1,382 @@ +//! Yu-Ris YSVR(Variables) file (.ybn) +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 anyhow::Result; +use serde::{Deserialize, Serialize}; +use std::any::Any; +use std::io::{Read, Seek, Write}; + +#[derive(Debug, Serialize, Deserialize)] +struct YSVRData { + version: u32, + variables: Vec, +} + +impl StructUnpack for YSVRData { + fn unpack( + reader: &mut R, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result { + let version = u32::unpack(reader, big, encoding, info)?; + let ninfo = Box::new(version) as Box; + let count = u16::unpack(reader, big, encoding, info)?; + let variables = reader.read_struct_vec(count as usize, big, encoding, &Some(ninfo))?; + Ok(Self { version, variables }) + } +} + +impl StructPack for YSVRData { + fn pack( + &self, + writer: &mut W, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result<()> { + self.version.pack(writer, big, encoding, info)?; + let ninfo = Box::new(self.version) as Box; + let count = self.variables.len() as u16; + count.pack(writer, big, encoding, info)?; + let info = &Some(ninfo); + for variable in &self.variables { + variable.pack(writer, big, encoding, info)?; + } + Ok(()) + } +} + +fn get_info_as_version(info: &Option>) -> Result { + Ok(*info + .as_ref() + .ok_or_else(|| anyhow::anyhow!("info not found"))? + .downcast_ref() + .ok_or_else(|| anyhow::anyhow!("not YSVR version"))?) +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(untagged)] +enum VariableValue { + None, + Int(i64), + Double(f64), + ByteString { raw: Base64Bytes }, + MString(String), +} + +impl StructUnpack for VariableValue { + fn unpack( + reader: &mut R, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result { + let var_type = *info + .as_ref() + .and_then(|i| i.downcast_ref::()) + .ok_or_else(|| anyhow::anyhow!("VariableValue: type info missing"))?; + match var_type { + 0 => Ok(Self::None), + 1 => { + let v = i64::unpack(reader, big, encoding, info)?; + Ok(Self::Int(v)) + } + 2 => { + let v = f64::unpack(reader, big, encoding, info)?; + Ok(Self::Double(v)) + } + 3 => { + let len = u16::unpack(reader, big, encoding, info)? as usize; + let mut buf = vec![0u8; len]; + reader.read_exact(&mut buf)?; + if buf.starts_with(b"M") && buf.len() >= 3 { + let len = u16::from_le_bytes([buf[1], buf[2]]); + if buf.len() >= len as usize + 3 { + if let Ok(s) = decode_to_string(encoding, &buf[3..], true) { + return Ok(Self::MString(s)); + } + } + } + Ok(Self::ByteString { raw: buf.into() }) + } + _ => anyhow::bail!( + "Unknown variable type: {} at {}", + var_type, + reader.stream_position()? + ), + } + } +} + +impl StructPack for VariableValue { + fn pack( + &self, + writer: &mut W, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result<()> { + match self { + Self::Int(v) => v.pack(writer, big, encoding, info)?, + Self::Double(v) => v.pack(writer, big, encoding, info)?, + Self::ByteString { raw } => { + let len = raw.len() as u16; + len.pack(writer, big, encoding, info)?; + writer.write_all(&raw)?; + } + Self::MString(s) => { + let encoded = encode_string(encoding, &s, true)?; + let len = encoded.len() as u16; + (len + 3).pack(writer, big, encoding, info)?; + writer.write_u8(b'M')?; + writer.write_u16(len)?; + writer.write_all(&encoded)?; + } + Self::None => {} + } + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +struct Variable { + scope: u8, + // #TODO: Better version handle + #[serde(skip_serializing_if = "Option::is_none")] + unk: Option, + script_id: u16, + variable_index: u16, + dimension_sizes: Vec, + value: VariableValue, +} + +impl StructUnpack for Variable { + fn unpack( + reader: &mut R, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result { + let version = get_info_as_version(info)?; + + let scope = reader.read_u8()?; + let unk = if version >= 500 { + Some(reader.read_u8()?) + } else { + None + }; + let script_id = reader.read_u16()?; + let variable_index = reader.read_u16()?; + let variable_type = reader.read_u8()?; + let num_dimensions = reader.read_u8()?; + let mut dimension_sizes = Vec::with_capacity(num_dimensions as usize); + for _ in 0..num_dimensions { + dimension_sizes.push(reader.read_u32()?); + } + let value_info = Box::new(variable_type) as Box; + let value = VariableValue::unpack(reader, big, encoding, &Some(value_info))?; + + Ok(Self { + scope, + unk, + script_id, + variable_index, + dimension_sizes, + value, + }) + } +} + +impl StructPack for Variable { + fn pack( + &self, + writer: &mut W, + big: bool, + encoding: Encoding, + info: &Option>, + ) -> Result<()> { + let version = get_info_as_version(info)?; + + writer.write_u8(self.scope)?; + if version >= 500 { + writer.write_u8(self.unk.unwrap_or(0))?; + } + writer.write_u16(self.script_id)?; + writer.write_u16(self.variable_index)?; + writer.write_u8(match &self.value { + VariableValue::None => 0, + VariableValue::Int(_) => 1, + VariableValue::Double(_) => 2, + VariableValue::ByteString { .. } => 3, + VariableValue::MString(_) => 3, + })?; + writer.write_u8(self.dimension_sizes.len() as u8)?; + for &size in &self.dimension_sizes { + writer.write_u32(size)?; + } + self.value.pack(writer, big, encoding, &None)?; + Ok(()) + } +} + +#[derive(Debug)] +pub struct YSVRBuilder {} + +impl YSVRBuilder { + /// Creates a new instance of `YSVRBuilder` + pub const fn new() -> Self { + YSVRBuilder {} + } +} + +impl ScriptBuilder for YSVRBuilder { + fn default_encoding(&self) -> Encoding { + Encoding::Cp932 + } + + fn build_script( + &self, + buf: Vec, + _filename: &str, + encoding: Encoding, + _archive_encoding: Encoding, + config: &ExtraConfig, + _archive: Option<&Box>, + ) -> Result> { + Ok(Box::new(YSVR::new(MemReader::new(buf), encoding, config)?)) + } + + fn extensions(&self) -> &'static [&'static str] { + &["ybn"] + } + + fn is_this_format(&self, _filename: &str, buf: &[u8], buf_len: usize) -> Option { + if buf_len >= 4 && buf.starts_with(b"YSVR") { + return Some(20); + } + None + } + + fn script_type(&self) -> &'static ScriptType { + &ScriptType::YurisYSVR + } + + fn can_create_file(&self) -> bool { + true + } + + fn create_file<'a>( + &'a self, + filename: &'a str, + writer: Box, + encoding: Encoding, + file_encoding: Encoding, + config: &ExtraConfig, + ) -> Result<()> { + create_file( + filename, + writer, + encoding, + file_encoding, + config.custom_yaml, + ) + } +} + +#[derive(Debug)] +pub struct YSVR { + data: YSVRData, + custom_yaml: bool, +} + +impl YSVR { + pub fn new( + mut reader: T, + encoding: Encoding, + config: &ExtraConfig, + ) -> Result { + let mut sig = [0; 4]; + reader.read_exact(&mut sig)?; + if &sig != b"YSVR" { + anyhow::bail!("Unsupported YSVR file."); + } + let data = YSVRData::unpack(&mut reader, false, encoding, &None)?; + Ok(Self { + data, + custom_yaml: config.custom_yaml, + }) + } +} + +impl Script for YSVR { + fn default_output_script_type(&self) -> OutputScriptType { + OutputScriptType::Custom + } + + fn is_output_supported(&self, output: OutputScriptType) -> bool { + matches!(output, OutputScriptType::Custom) + } + + fn default_format_type(&self) -> FormatOptions { + FormatOptions::None + } + + fn custom_output_extension(&self) -> &'static str { + if self.custom_yaml { "yaml" } else { "json" } + } + + fn custom_export(&self, filename: &std::path::Path, encoding: Encoding) -> Result<()> { + let s = if self.custom_yaml { + serde_yaml_ng::to_string(&self.data) + .map_err(|e| anyhow::anyhow!("Failed to serialize to YAML: {}", e))? + } else { + serde_json::to_string_pretty(&self.data) + .map_err(|e| anyhow::anyhow!("Failed to serialize to JSON: {}", e))? + }; + let mut writer = crate::utils::files::write_file(filename)?; + let s = encode_string(encoding, &s, false)?; + writer.write_all(&s)?; + writer.flush()?; + Ok(()) + } + + fn custom_import<'a>( + &'a self, + custom_filename: &'a str, + file: Box, + encoding: Encoding, + output_encoding: Encoding, + ) -> Result<()> { + create_file( + custom_filename, + file, + encoding, + output_encoding, + self.custom_yaml, + ) + } +} + +fn create_file<'a>( + custom_filename: &'a str, + mut writer: Box, + encoding: Encoding, + output_encoding: Encoding, + yaml: bool, +) -> Result<()> { + let input = crate::utils::files::read_file(custom_filename)?; + let s = decode_to_string(output_encoding, &input, true)?; + let data: YSVRData = if yaml { + serde_yaml_ng::from_str(&s).map_err(|e| anyhow::anyhow!("Failed to parse YAML: {}", e))? + } else { + serde_json::from_str(&s).map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}", e))? + }; + writer.write_all(b"YSVR")?; + data.pack(&mut writer, false, encoding, &None)?; + Ok(()) +} diff --git a/src/types.rs b/src/types.rs index 67b3d09..d0c7ba4 100644 --- a/src/types.rs +++ b/src/types.rs @@ -938,6 +938,9 @@ pub enum ScriptType { #[cfg(feature = "yuris")] /// Yu-Ris YSLB(labels) file (.ybn) YurisYSLB, + #[cfg(feature = "yuris")] + /// Yu-Ris YSVR(Variables) file (.ybn) + YurisYSVR, #[cfg(feature = "yuris-img")] /// YU-RIS compressed image file (.ydg) YurisYDG,