From 2fe915a0c047a20e665ae484868918d9864ec071 Mon Sep 17 00:00:00 2001 From: lifegpc Date: Sat, 31 Jan 2026 20:07:04 +0800 Subject: [PATCH] Add import support for Qlie Abmp10/11/12 image (.b) --- README.md | 2 +- src/ext/io.rs | 52 +++++- src/scripts/qlie/image/abmp10.rs | 298 +++++++++++++++++++++++++++++++ 3 files changed, 350 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index ae1d9f4..44b0794 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ msg-tool create -t | Script Type | Feature Name | Name | Export | Import | Export Multiple | Import Multiple | Custom Export | Custom Import | Create | Remarks | |---|---|---|---|---|---|---|---|---|---|---| | `qlie` | `qlie` | Qlie Engine Scenario script (.s) | ✔️ | ✔️ | ❌ | ❌ | ❌ | ❌ | ❌ | | -| `qlie-abmp10` / `qlie-abmp11` / `qlie-abmp12` | `qlie-img` | Qlie Abmp10/11/12 image (.b) | ❌ | ❌ | ❌ | ❌ | ✔️ | ❌ | ❌ | | +| `qlie-abmp10` / `qlie-abmp11` / `qlie-abmp12` | `qlie-img` | Qlie Abmp10/11/12 image (.b) | ❌ | ❌ | ❌ | ❌ | ✔️ | ✔️ | ✔️ | | | Archive Type | Feature Name | Name | Unpack | Pack | Remarks | |---|---|---|---|---|---| diff --git a/src/ext/io.rs b/src/ext/io.rs index c2b60e7..9350026 100644 --- a/src/ext/io.rs +++ b/src/ext/io.rs @@ -1,7 +1,7 @@ //!Extensions for IO operations. use crate::scripts::base::ReadSeek; use crate::types::Encoding; -use crate::utils::encoding::decode_to_string; +use crate::utils::encoding::{decode_to_string, encode_string}; use crate::utils::struct_pack::{StructPack, StructUnpack}; use std::ffi::CString; use std::io::*; @@ -1180,6 +1180,20 @@ pub trait WriteExt { /// Writes a C-style string (null-terminated) to the writer. fn write_cstring(&mut self, value: &CString) -> Result<()>; + /// Writes a C-style string (null-terminated) from the reader with maximum length. + /// * `data` is the string data to write. + /// * `len` is the maximum length of the string to write. If the string is longer, it will be truncated if `truncate` is true otherwise an error is returned. + /// * `encoding` specifies the encoding to use for the string. + /// * `padding` indicates how to pad the string if it's shorter than `len`. + /// * `truncate` indicates whether to truncate the string if it's longer than `len`. + fn write_fstring( + &mut self, + data: &str, + len: usize, + encoding: Encoding, + padding: u8, + truncate: bool, + ) -> Result<()>; /// Write a struct to the writer. fn write_struct( &mut self, @@ -1264,6 +1278,42 @@ impl WriteExt for T { self.write_all(value.as_bytes_with_nul()) } + fn write_fstring( + &mut self, + data: &str, + len: usize, + encoding: Encoding, + padding: u8, + truncate: bool, + ) -> Result<()> { + let encoded = encode_string(encoding, data, true).map_err(|e| { + std::io::Error::new( + std::io::ErrorKind::InvalidInput, + format!("Encoding error: {}", e), + ) + })?; + let final_data = if encoded.len() > len { + if truncate { + &encoded[..len] + } else { + return Err(std::io::Error::new( + std::io::ErrorKind::InvalidInput, + "String length exceeds the specified length and truncation is disabled", + )); + } + } else { + &encoded + }; + self.write_all(final_data)?; + let padding_len = len.saturating_sub(final_data.len()); + if padding_len > 0 { + for _ in 0..padding_len { + self.write_u8(padding)?; + } + } + Ok(()) + } + fn write_struct( &mut self, value: &V, diff --git a/src/scripts/qlie/image/abmp10.rs b/src/scripts/qlie/image/abmp10.rs index c14c08b..bc4354c 100644 --- a/src/scripts/qlie/image/abmp10.rs +++ b/src/scripts/qlie/image/abmp10.rs @@ -56,6 +56,21 @@ impl ScriptBuilder for Abmp10ImageBuilder { } None } + + 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) + } } trait AbmpRes { @@ -66,6 +81,12 @@ trait AbmpRes { ) -> Result where Self: Sized; + fn write_to( + &self, + data: &mut T, + encoding: Encoding, + img: &AbmpImage, + ) -> Result<()>; } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -103,6 +124,22 @@ impl AbmpRes for AbData { data: ResourceRef { index }, }) } + + fn write_to( + &self, + data: &mut T, + encoding: Encoding, + img: &AbmpImage, + ) -> Result<()> { + data.write_fstring(&self.tag, 0x10, encoding, 0, false)?; + let res = img + .resources + .get(self.data.index) + .ok_or_else(|| anyhow::anyhow!("Resource index {} out of bounds", self.data.index))?; + data.write_u32(res.len() as u32)?; + data.write_all(res)?; + Ok(()) + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -132,6 +169,20 @@ impl AbmpRes for AbImage10 { } Ok(AbImage10 { datas }) } + + fn write_to( + &self, + data: &mut T, + encoding: Encoding, + img: &AbmpImage, + ) -> Result<()> { + data.write_fstring("abimage10", 0x10, encoding, 0, false)?; + data.write_u8(self.datas.len() as u8)?; + for res in &self.datas { + res.write_to(data, encoding, img)?; + } + Ok(()) + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -161,6 +212,20 @@ impl AbmpRes for AbSound10 { } Ok(AbSound10 { datas }) } + + fn write_to( + &self, + data: &mut T, + encoding: Encoding, + img: &AbmpImage, + ) -> Result<()> { + data.write_fstring("absound10", 0x10, encoding, 0, false)?; + data.write_u8(self.datas.len() as u8)?; + for res in &self.datas { + res.write_to(data, encoding, img)?; + } + Ok(()) + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -227,6 +292,39 @@ impl AbmpRes for AbImgData15 { data: ResourceRef { index }, }) } + + fn write_to( + &self, + data: &mut T, + encoding: Encoding, + img: &AbmpImage, + ) -> Result<()> { + data.write_fstring("abimgdat15", 0x10, encoding, 0, false)?; + data.write_u32(self.version)?; + let name_length = self.name.encode_utf16().count() as u16; + let name = encode_string(Encoding::Utf16LE, &self.name, true)?; + if name.len() != (name_length as usize) * 2 { + anyhow::bail!("Name length mismatch when writing AbImgData15"); + } + data.write_u16(name_length)?; + data.write_all(&name)?; + let internal_name = encode_string(encoding, &self.internal_name, true)?; + data.write_u16(internal_name.len() as u16)?; + data.write_all(&internal_name)?; + data.write_u8(self.typ)?; + let param_size = if self.version == 2 { 0x1d } else { 0x11 }; + if self.param.len() != param_size { + anyhow::bail!("Param size mismatch when writing AbImgData15"); + } + data.write_all(&self.param)?; + let res = img + .resources + .get(self.data.index) + .ok_or_else(|| anyhow::anyhow!("Resource index {} out of bounds", self.data.index))?; + data.write_u32(res.len() as u32)?; + data.write_all(res)?; + Ok(()) + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -289,6 +387,33 @@ impl AbmpRes for AbImgData14 { data: ResourceRef { index }, }) } + + fn write_to( + &self, + data: &mut T, + encoding: Encoding, + img: &AbmpImage, + ) -> Result<()> { + data.write_fstring("abimgdat14", 0x10, encoding, 0, false)?; + let name = encode_string(encoding, &self.name, true)?; + data.write_u16(name.len() as u16)?; + data.write_all(&name)?; + let internal_name = encode_string(encoding, &self.internal_name, true)?; + data.write_u16(internal_name.len() as u16)?; + data.write_all(&internal_name)?; + data.write_u8(self.typ)?; + if self.param.len() != 0x4C { + anyhow::bail!("Param size mismatch when writing AbImgData14"); + } + data.write_all(&self.param)?; + let res = img + .resources + .get(self.data.index) + .ok_or_else(|| anyhow::anyhow!("Resource index {} out of bounds", self.data.index))?; + data.write_u32(res.len() as u32)?; + data.write_all(res)?; + Ok(()) + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -351,6 +476,33 @@ impl AbmpRes for AbImgData13 { data: ResourceRef { index }, }) } + + fn write_to( + &self, + data: &mut T, + encoding: Encoding, + img: &AbmpImage, + ) -> Result<()> { + data.write_fstring("abimgdat13", 0x10, encoding, 0, false)?; + let name = encode_string(encoding, &self.name, true)?; + data.write_u16(name.len() as u16)?; + data.write_all(&name)?; + let internal_name = encode_string(encoding, &self.internal_name, true)?; + data.write_u16(internal_name.len() as u16)?; + data.write_all(&internal_name)?; + data.write_u8(self.typ)?; + if self.param.len() != 0xC { + anyhow::bail!("Param size mismatch when writing AbImgData13"); + } + data.write_all(&self.param)?; + let res = img + .resources + .get(self.data.index) + .ok_or_else(|| anyhow::anyhow!("Resource index {} out of bounds", self.data.index))?; + data.write_u32(res.len() as u32)?; + data.write_all(res)?; + Ok(()) + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -399,6 +551,33 @@ impl AbmpRes for AbSndData12 { data: ResourceRef { index }, }) } + + fn write_to( + &self, + data: &mut T, + encoding: Encoding, + img: &AbmpImage, + ) -> Result<()> { + data.write_fstring("absnddat12", 0x10, encoding, 0, false)?; + data.write_u32(self.version)?; + let name_length = self.name.encode_utf16().count() as u16; + let name = encode_string(Encoding::Utf16LE, &self.name, true)?; + if name.len() != (name_length as usize) * 2 { + anyhow::bail!("Name length mismatch when writing AbSndData12"); + } + data.write_u16(name_length)?; + data.write_all(&name)?; + let internal_name = encode_string(encoding, &self.internal_name, true)?; + data.write_u16(internal_name.len() as u16)?; + data.write_all(&internal_name)?; + let res = img + .resources + .get(self.data.index) + .ok_or_else(|| anyhow::anyhow!("Resource index {} out of bounds", self.data.index))?; + data.write_u32(res.len() as u32)?; + data.write_all(res)?; + Ok(()) + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -444,6 +623,28 @@ impl AbmpRes for AbSndData11 { data: ResourceRef { index }, }) } + + fn write_to( + &self, + data: &mut T, + encoding: Encoding, + img: &AbmpImage, + ) -> Result<()> { + data.write_fstring("absnddat11", 0x10, encoding, 0, false)?; + let name = encode_string(encoding, &self.name, true)?; + data.write_u16(name.len() as u16)?; + data.write_all(&name)?; + let internal_name = encode_string(encoding, &self.internal_name, true)?; + data.write_u16(internal_name.len() as u16)?; + data.write_all(&internal_name)?; + let res = img + .resources + .get(self.data.index) + .ok_or_else(|| anyhow::anyhow!("Resource index {} out of bounds", self.data.index))?; + data.write_u32(res.len() as u32)?; + data.write_all(res)?; + Ok(()) + } } #[derive(Clone, Debug, Serialize, Deserialize)] @@ -496,6 +697,24 @@ impl AbmpRes for AbmpResource { } } } + + fn write_to( + &self, + data: &mut T, + encoding: Encoding, + img: &AbmpImage, + ) -> Result<()> { + match self { + AbmpResource::Data(res) => res.write_to(data, encoding, img), + AbmpResource::Image10(res) => res.write_to(data, encoding, img), + AbmpResource::ImgData15(res) => res.write_to(data, encoding, img), + AbmpResource::ImgData14(res) => res.write_to(data, encoding, img), + AbmpResource::ImgData13(res) => res.write_to(data, encoding, img), + AbmpResource::Sound10(res) => res.write_to(data, encoding, img), + AbmpResource::SndData12(res) => res.write_to(data, encoding, img), + AbmpResource::SndData11(res) => res.write_to(data, encoding, img), + } + } } /// Qlie Abmp10/11/12 image @@ -559,6 +778,21 @@ impl AbmpImage { Ok(img) } + pub fn dump_to(&self, mut writer: T, encoding: Encoding) -> Result<()> { + writer.write_fstring( + &format!("abmp1{}", (self.version - 10 + b'0') as char), + 16, + encoding, + 0, + false, + )?; + for data in &self.datas { + data.write_to(&mut writer, encoding, self)?; + } + writer.write_all(&self.extra)?; + Ok(()) + } + fn to_image2(&self) -> AbmpImage2 { AbmpImage2 { version: self.version, @@ -567,6 +801,16 @@ impl AbmpImage { extra: self.extra.clone(), } } + + fn from_image2(img: &AbmpImage2) -> Self { + AbmpImage { + version: img.version, + datas: img.datas.clone(), + resources: Vec::new(), + resource_filenames: Vec::new(), + extra: img.extra.clone(), + } + } } #[derive(Debug)] @@ -670,4 +914,58 @@ impl Script for Abmp10Image { file.write_all(&s)?; 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.config, + ) + } +} + +fn create_file<'a>( + filename: &str, + mut writer: Box, + encoding: Encoding, + file_encoding: Encoding, + config: &ExtraConfig, +) -> Result<()> { + let data = crate::utils::files::read_file(filename)?; + let s = decode_to_string(file_encoding, &data, true)?; + let img2: AbmpImage2 = if config.custom_yaml { + serde_yaml_ng::from_str(&s)? + } else { + serde_json::from_str(&s)? + }; + let mut img = AbmpImage::from_image2(&img2); + let mut base_path = std::path::PathBuf::from(filename); + base_path.set_extension(""); + for res in &img2.resources { + let path = base_path.join(&res.path); + let buf = if res.ambp10 { + let mut mem = MemWriter::new(); + create_file( + &path.to_string_lossy(), + Box::new(&mut mem), + encoding, + file_encoding, + config, + )?; + mem.into_inner() + } else { + crate::utils::files::read_file(&path)? + }; + img.resources.push(buf); + } + img.dump_to(&mut writer, encoding)?; + Ok(()) }