Add support for Yu-Ris YSTL(file list) file (.ybn)

This commit is contained in:
2026-06-01 13:10:58 +08:00
parent 807c53511d
commit 06936c821a
7 changed files with 570 additions and 1 deletions

215
Cargo.lock generated
View File

@@ -46,6 +46,15 @@ version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
[[package]]
name = "android_system_properties"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "1.0.0"
@@ -330,6 +339,20 @@ dependencies = [
"rand_core",
]
[[package]]
name = "chrono"
version = "0.4.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c673075a2e0e5f4a1dde27ce9dee1ea4558c7ffe648f576438a20ca1d2acc4b0"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"serde",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "cipher"
version = "0.5.1"
@@ -474,6 +497,12 @@ dependencies = [
"tiny-keccak",
]
[[package]]
name = "core-foundation-sys"
version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
[[package]]
name = "cpubits"
version = "0.1.1"
@@ -923,6 +952,30 @@ dependencies = [
"syn 1.0.109",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
@@ -1069,6 +1122,30 @@ dependencies = [
"typenum",
]
[[package]]
name = "iana-time-zone"
version = "0.1.65"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.2.0"
@@ -1325,6 +1402,18 @@ dependencies = [
"libc",
]
[[package]]
name = "js-sys"
version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
dependencies = [
"cfg-if",
"futures-util",
"once_cell",
"wasm-bindgen",
]
[[package]]
name = "json"
version = "0.12.4"
@@ -1534,6 +1623,7 @@ dependencies = [
"bytes",
"cbc",
"chacha20",
"chrono",
"clap 4.6.1",
"crc32fast",
"crossbeam",
@@ -1661,6 +1751,15 @@ dependencies = [
"memchr",
]
[[package]]
name = "num-traits"
version = "0.2.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
dependencies = [
"autocfg",
]
[[package]]
name = "num_cpus"
version = "1.17.0"
@@ -1824,6 +1923,12 @@ dependencies = [
"siphasher",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.33"
@@ -2051,6 +2156,12 @@ version = "2.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
[[package]]
name = "rustversion"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
[[package]]
name = "ryu"
version = "1.0.23"
@@ -2202,6 +2313,12 @@ version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
@@ -2594,6 +2711,51 @@ dependencies = [
"wit-bindgen 0.51.0",
]
[[package]]
name = "wasm-bindgen"
version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
dependencies = [
"cfg-if",
"once_cell",
"rustversion",
"wasm-bindgen-macro",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-macro"
version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
]
[[package]]
name = "wasm-bindgen-macro-support"
version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
dependencies = [
"bumpalo",
"proc-macro2",
"quote",
"syn 2.0.117",
"wasm-bindgen-shared",
]
[[package]]
name = "wasm-bindgen-shared"
version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
dependencies = [
"unicode-ident",
]
[[package]]
name = "wasm-encoder"
version = "0.244.0"
@@ -2680,12 +2842,65 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.117",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.59.0"

View File

@@ -18,6 +18,7 @@ byteorder = { version = "1.5", default-features = false, optional = true}
bytes = { version = "1.11", optional = true }
cbc = { version = "0.2", optional = true }
chacha20 = { version = "0.10", optional = true }
chrono = { version = "0.4", optional = true, features = ["serde"] }
clap = { version = "4.5", features = ["derive"] }
crc32fast = { version = "1.5", optional = true }
crossbeam = { version = "0.8", optional = true }
@@ -118,7 +119,7 @@ will-plus = ["utils-str"]
will-plus-img = ["will-plus", "image"]
yaneurao = []
yaneurao-itufuru = ["yaneurao", "utils-xored-stream"]
yuris = ["dep:hex", "utils-serde-base64bytes", "utils-xored-stream"]
yuris = ["dep:chrono", "dep:hex", "utils-serde-base64bytes", "utils-xored-stream"]
yuris-img = ["yuris", "image", "qoi", "webp"]
# basic feature
image = ["dep:png"]

View File

@@ -271,6 +271,7 @@ msg-tool create -t <script-type> <input> <output>
| `yuris-yscfg` | `yuris` | Yu-Ris YSCFG(config) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | |
| `yuris-ystb` | `yuris` | Yu-Ris YSTB(compiled script) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ❌ | |
| `yuris-txt` | `yuris` | Yu-Ris scenario text file (.txt) | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | |
| `yuris-ystl` | `yuris` | Yu-Ris YSTL(file list) file (.ybn) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | |
| Image Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Create | Remarks |
|---|---|---|---|---|---|---|---|---|

View File

@@ -188,6 +188,8 @@ lazy_static::lazy_static! {
Box::new(yuris::txt::YurisTxtBuilder::new()),
#[cfg(feature = "yuris-img")]
Box::new(yuris::img::ydg::YDGImageBuilder::new()),
#[cfg(feature = "yuris")]
Box::new(yuris::ystl::YSTLBuilder::new()),
];
/// A list of all script extensions.
pub static ref ALL_EXTS: Vec<String> =

View File

@@ -7,3 +7,4 @@ pub mod yscfg;
pub mod yscm;
pub mod yser;
pub mod ystb;
pub mod ystl;

347
src/scripts/yuris/ystl.rs Normal file
View File

@@ -0,0 +1,347 @@
//! Yu-Ris YSTL(file list) file (.ybn)
use crate::ext::io::*;
use crate::scripts::base::*;
use crate::types::*;
use crate::utils::encoding::*;
use crate::utils::struct_pack::*;
use anyhow::Result;
use chrono::TimeZone;
use chrono::Timelike;
use chrono::{DateTime, Local, Utc};
use msg_tool_macro::*;
use serde::{Deserialize, Serialize};
use std::any::Any;
use std::io::{Read, Seek, Write};
use std::ops::{Deref, DerefMut};
#[derive(Debug, Serialize, Deserialize)]
struct YSTLData {
version: u32,
entries: Vec<YSTLEntry>,
}
impl StructUnpack for YSTLData {
fn unpack<R: Read + Seek>(
reader: &mut R,
big: bool,
encoding: Encoding,
info: &Option<Box<dyn Any>>,
) -> Result<Self> {
let version = u32::unpack(reader, big, encoding, info)?;
let ninfo = Box::new(version) as Box<dyn Any>;
let count = u32::unpack(reader, big, encoding, info)?;
let entries = reader.read_struct_vec(count as usize, big, encoding, &Some(ninfo))?;
Ok(Self { version, entries })
}
}
impl StructPack for YSTLData {
fn pack<W: Write>(
&self,
writer: &mut W,
big: bool,
encoding: Encoding,
info: &Option<Box<dyn Any>>,
) -> Result<()> {
self.version.pack(writer, big, encoding, info)?;
let ninfo = Box::new(self.version) as Box<dyn Any>;
let count = self.entries.len() as u32;
count.pack(writer, big, encoding, info)?;
let info = &Some(ninfo);
for (i, entry) in self.entries.iter().enumerate() {
if entry.seq == i as u32 {
entry.pack(writer, big, encoding, info)?;
} else {
let mut entry = entry.clone();
entry.seq = i as u32;
entry.pack(writer, big, encoding, info)?;
}
}
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(transparent)]
struct FileSystemTime(DateTime<Local>);
impl Deref for FileSystemTime {
type Target = DateTime<Local>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for FileSystemTime {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl std::fmt::Display for FileSystemTime {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl StructUnpack for FileSystemTime {
fn unpack<R: Read + Seek>(
reader: &mut R,
big: bool,
encoding: Encoding,
info: &Option<Box<dyn std::any::Any>>,
) -> Result<Self> {
let high = u32::unpack(reader, big, encoding, info)?;
let low = u32::unpack(reader, big, encoding, info)?;
let time = low as u64 | ((high as u64) << 32);
const FILETIME_OFFSET: u64 = 116_444_736_000_000_000;
if time < FILETIME_OFFSET {
anyhow::bail!("Time to small.");
}
let intervals_since_1970 = time - FILETIME_OFFSET;
let seconds = (intervals_since_1970 / 10_000_000) as i64;
let nsecs = ((intervals_since_1970 % 10_000_000) * 100) as u32;
let time = Utc
.timestamp_opt(seconds, nsecs)
.single()
.ok_or_else(|| anyhow::anyhow!("Time is not existed or ambiguous."))?;
let time = time.with_timezone(&Local);
Ok(Self(time))
}
}
impl StructPack for FileSystemTime {
fn pack<W: Write>(
&self,
writer: &mut W,
big: bool,
encoding: Encoding,
info: &Option<Box<dyn std::any::Any>>,
) -> Result<()> {
let time = self.0.with_timezone(&Utc);
let tseconds = time.timestamp();
let nsecs = time.nanosecond() / 100;
const FILETIME_OFFSET: u64 = 116_444_736_000_000_000;
let seconds = (tseconds as u64)
.checked_mul(10_000_000)
.ok_or_else(|| anyhow::anyhow!("Too big time"))?
.checked_add(nsecs as u64)
.ok_or_else(|| anyhow::anyhow!("Too big time"))?
.checked_add(FILETIME_OFFSET)
.ok_or_else(|| anyhow::anyhow!("Too big time"))?;
let high = (seconds >> 32) as u32;
let low = seconds as u32;
high.pack(writer, big, encoding, info)?;
low.pack(writer, big, encoding, info)?;
Ok(())
}
}
#[derive(Debug, Clone, Deserialize, Serialize, StructUnpack, StructPack)]
struct YSTLEntry {
seq: u32,
#[pstring(u32)]
path: String,
modification_time: FileSystemTime,
num_variables: u32,
num_labels: u32,
// TODO: version may need more check
#[skip_pack_if(get_info_as_version(__info)? < 300)]
#[skip_unpack_if(get_info_as_version(__info)? < 300)]
num_texts: u32,
}
fn get_info_as_version(info: &Option<Box<dyn Any>>) -> Result<u32> {
Ok(*info
.as_ref()
.ok_or_else(|| anyhow::anyhow!("info not found"))?
.downcast_ref()
.ok_or_else(|| anyhow::anyhow!("not YSTBHeader"))?)
}
#[derive(Debug)]
pub struct YSTLBuilder {}
impl YSTLBuilder {
/// Creates a new instance of `YSTLBuilder`
pub const fn new() -> Self {
YSTLBuilder {}
}
}
impl ScriptBuilder for YSTLBuilder {
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 + Send + Sync>> {
Ok(Box::new(YSTL::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<u8> {
if buf_len >= 4 && buf.starts_with(b"YSTL") {
return Some(20);
}
None
}
fn script_type(&self) -> &'static ScriptType {
&ScriptType::YurisYSTL
}
fn can_create_file(&self) -> bool {
true
}
fn create_file<'a>(
&'a self,
filename: &'a str,
writer: Box<dyn WriteSeek + 'a>,
encoding: Encoding,
file_encoding: Encoding,
config: &ExtraConfig,
) -> Result<()> {
create_file(
filename,
writer,
encoding,
file_encoding,
config.custom_yaml,
)
}
}
#[derive(Debug)]
pub struct YSTL {
data: YSTLData,
custom_yaml: bool,
}
impl YSTL {
pub fn new<T: Read + Seek>(
mut reader: T,
encoding: Encoding,
config: &ExtraConfig,
) -> Result<Self> {
let mut sig = [0; 4];
reader.read_exact(&mut sig)?;
if &sig != b"YSTL" {
anyhow::bail!("Unsupported YSTL file.");
}
let data = YSTLData::unpack(&mut reader, false, encoding, &None)?;
Ok(Self {
data,
custom_yaml: config.custom_yaml,
})
}
}
impl Script for YSTL {
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<dyn WriteSeek + 'a>,
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<dyn WriteSeek + 'a>,
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 mut data: YSTLData = 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"YSTL")?;
writer.write_u32(data.version)?;
writer.write_u32(data.entries.len() as u32)?;
let info = Box::new(data.version) as Box<dyn Any>;
let info = &Some(info);
for (i, entry) in data.entries.iter_mut().enumerate() {
entry.seq = i as u32;
entry.pack(&mut writer, false, encoding, info)?;
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unpack_file_system_time() {
let mut reader = MemReaderRef::new(b" \x0c\xd8\x01\x00k`\xdd");
let ts = FileSystemTime::unpack(&mut reader, false, Encoding::Cp932, &None).unwrap();
println!("{}", ts);
let utc = ts.to_utc();
let t = serde_json::to_string(&utc).unwrap();
assert_eq!(t, "\"2022-01-18T04:07:10Z\"");
}
#[test]
fn test_pack_file_system_time() {
let ts: FileSystemTime = serde_json::from_str("\"2022-01-18T13:07:10+09:00\"").unwrap();
let mut buf = [0; 8];
let mut writer = MemWriterRef::new(&mut buf);
ts.pack(&mut writer, false, Encoding::Cp932, &None).unwrap();
assert_eq!(&buf, b" \x0c\xd8\x01\x00k`\xdd");
}
}

View File

@@ -929,6 +929,8 @@ pub enum ScriptType {
#[cfg(feature = "yuris")]
/// Yu-Ris scenario text file (.txt)
YurisTxt,
/// Yu-Ris YSTL(file list) file (.ybn)
YurisYSTL,
#[cfg(feature = "yuris-img")]
/// YU-RIS compressed image file (.ydg)
YurisYDG,