Compare commits

..

No commits in common. "main" and "utils-0.1.2" have entirely different histories.

31 changed files with 413 additions and 1305 deletions

View file

@ -35,7 +35,6 @@ jobs:
- name: '🔄 Set up additional requirements' - name: '🔄 Set up additional requirements'
run: | run: |
sudo apt-get install -y gcc-mingw-w64 sudo apt-get install -y gcc-mingw-w64
sudo apt-get install -y libasound2t64
pip install cargo-zigbuild pip install cargo-zigbuild
- name: '📦 Package Windows x86_64' - name: '📦 Package Windows x86_64'

View file

@ -3,7 +3,7 @@ resolver = "2"
members = [ members = [
"cz", "cz",
"pak_explorer", "pak_explorer",
"luca_pak", "utils", "luca_script", "luca_pak", "utils",
] ]
[workspace.package] [workspace.package]

View file

@ -1,9 +1,4 @@
<p align="center"> # lbee-utils
<img width="80%" src="https://github.com/user-attachments/assets/6807854b-aa4b-431d-933f-9e5b63ff5ed3">
</p>
# Lbee-Utils
A small collection of utilities for exporting and importing assets from games A small collection of utilities for exporting and importing assets from games
made with LUCA System by [Prototype Ltd](https://www.prot.co.jp/). made with LUCA System by [Prototype Ltd](https://www.prot.co.jp/).
@ -14,8 +9,6 @@ Tested on the following games:
- Kanon (2024) - Kanon (2024)
- planetarian \~Snow Globe~ (Nintendo Switch) (2024) - planetarian \~Snow Globe~ (Nintendo Switch) (2024)
Please test on your own games and open an issue if something isn't working.
## Acknowledgments ## Acknowledgments
The implementation of compresssion and decompression of CZ1, CZ2, CZ3, and CZ4 The implementation of compresssion and decompression of CZ1, CZ2, CZ3, and CZ4
was derived from [LuckSystem](https://github.com/wetor/LuckSystem). The was derived from [LuckSystem](https://github.com/wetor/LuckSystem). The
@ -52,8 +45,9 @@ metadata can't be changed as of yet, however.
Small command line tools for modifying CZ images and PAK archives. Small command line tools for modifying CZ images and PAK archives.
To install with Cargo: To install with Cargo:
``` ```
cargo install --git https://github.com/G2-Games/lbee-utils lbee-utils cargo install --git https://github.com/G2-Games/lbee-utils utils
``` ```
Otherwise, download the binaries from the Releases page here. Otherwise, download the binaries from the Releases page here.

View file

@ -9,11 +9,17 @@ Prototype Ltd.
license = "MIT" license = "MIT"
authors.workspace = true authors.workspace = true
[features]
png = ["dep:image"]
[dependencies] [dependencies]
byteorder-lite = "0.1" byteorder = "1.5"
thiserror = "2.0" thiserror = "1.0"
imagequant = "4.3" imagequant = "4.3"
rgb = "0.8" rgb = "0.8"
# Only active on PNG feature
image = { version = "0.25", optional = true }
[lints] [lints]
workspace = true workspace = true

View file

@ -1,80 +1,70 @@
pub struct BitIo { use std::io::{self, Read, Write};
data: Vec<u8>,
use byteorder::{ReadBytesExt, WriteBytesExt};
/// A simple way to write individual bits to an input implementing [Write].
pub struct BitWriter<'a, O: Write + WriteBytesExt> {
output: &'a mut O,
current_byte: u8,
byte_offset: usize, byte_offset: usize,
bit_offset: usize, bit_offset: usize,
byte_size: usize, byte_size: usize,
} }
impl BitIo { impl<'a, O: Write + WriteBytesExt> BitWriter<'a, O> {
/// Create a new BitIO reader and writer over some data /// Create a new BitWriter wrapper around something which
pub fn new(data: Vec<u8>) -> Self { /// implements [Write].
pub fn new(output: &'a mut O) -> Self {
Self { Self {
data, output,
current_byte: 0,
byte_offset: 0, byte_offset: 0,
bit_offset: 0, bit_offset: 0,
byte_size: 0, byte_size: 0,
} }
} }
/// Get the byte offset of the reader /// Get the number of whole bytes written to the stream.
pub fn byte_offset(&self) -> usize {
self.byte_offset
}
/// Get the byte size of the reader
pub fn byte_size(&self) -> usize { pub fn byte_size(&self) -> usize {
self.byte_size self.byte_size
} }
/// Get the current bytes up to `byte_size` in the reader /// Get the bit offset within the current byte.
pub fn bytes(&self) -> Vec<u8> { pub fn bit_offset(&self) -> u8 {
self.data[..self.byte_size].to_vec() self.bit_offset as u8
} }
/// Read some bits from the buffer /// Check if the stream is aligned to a byte.
pub fn read_bit(&mut self, bit_len: usize) -> u64 { pub fn aligned(&self) -> bool {
if bit_len > 8 * 8 { self.bit_offset() == 0
panic!("Cannot read more than 64 bits")
} }
if bit_len % 8 == 0 && self.bit_offset == 0 { /// Align the writer to the nearest byte by padding with zero bits.
return self.read(bit_len / 8); ///
} /// Returns the number of zero bits
pub fn flush(&mut self) -> Result<usize, io::Error> {
let mut result = 0;
for i in 0..bit_len {
let bit_value = ((self.data[self.byte_offset] as usize >> self.bit_offset) & 1) as u64;
self.bit_offset += 1;
if self.bit_offset == 8 {
self.byte_offset += 1; self.byte_offset += 1;
// Write out the current byte unfinished
self.output.write_u8(self.current_byte).unwrap();
self.current_byte = 0;
self.bit_offset = 0; self.bit_offset = 0;
Ok(8 - self.bit_offset)
} }
result |= bit_value << i; /// Write some bits to the output.
}
result
}
/// Read some bytes from the buffer
pub fn read(&mut self, byte_len: usize) -> u64 {
if byte_len > 8 {
panic!("Cannot read more than 8 bytes")
}
let mut padded_slice = [0u8; 8];
padded_slice.copy_from_slice(&self.data[self.byte_offset..self.byte_offset + byte_len]);
self.byte_offset += byte_len;
u64::from_le_bytes(padded_slice)
}
/// Write some bits to the buffer
pub fn write_bit(&mut self, data: u64, bit_len: usize) { pub fn write_bit(&mut self, data: u64, bit_len: usize) {
if bit_len > 8 * 8 { if bit_len > 64 {
panic!("Cannot write more than 64 bits"); panic!("Cannot write more than 64 bits at once.");
} else if bit_len == 0 {
panic!("Must write 1 or more bits.")
} }
if bit_len % 8 == 0 && self.bit_offset == 0 { if bit_len % 8 == 0 && self.bit_offset == 0 {
@ -85,32 +75,115 @@ impl BitIo {
for i in 0..bit_len { for i in 0..bit_len {
let bit_value = (data >> i) & 1; let bit_value = (data >> i) & 1;
self.data[self.byte_offset] &= !(1 << self.bit_offset); self.current_byte &= !(1 << self.bit_offset);
self.data[self.byte_offset] |= (bit_value << self.bit_offset) as u8; self.current_byte |= (bit_value << self.bit_offset) as u8;
self.bit_offset += 1; self.bit_offset += 1;
if self.bit_offset == 8 { if self.bit_offset >= 8 {
self.byte_offset += 1; self.byte_offset += 1;
self.bit_offset = 0; self.bit_offset = 0;
self.output.write_u8(self.current_byte).unwrap();
self.current_byte = 0;
} }
} }
self.byte_size = self.byte_offset + (self.bit_offset + 7) / 8; self.byte_size = self.byte_offset + (self.bit_offset + 7) / 8;
} }
/// Write some bytes to the output.
pub fn write(&mut self, data: u64, byte_len: usize) { pub fn write(&mut self, data: u64, byte_len: usize) {
if byte_len > 8 { if byte_len > 8 {
panic!("Cannot write more than 8 bytes") panic!("Cannot write more than 8 bytes at once.")
} else if byte_len == 0 {
panic!("Must write 1 or more bytes.")
} }
let mut padded_slice = [0u8; 8]; self.output
padded_slice.copy_from_slice(&data.to_le_bytes()); .write_all(&data.to_le_bytes()[..byte_len])
.unwrap();
self.data[self.byte_offset..self.byte_offset + byte_len]
.copy_from_slice(&padded_slice[..byte_len]);
self.byte_offset += byte_len; self.byte_offset += byte_len;
self.byte_size = self.byte_offset + (self.bit_offset + 7) / 8; self.byte_size = self.byte_offset + (self.bit_offset + 7) / 8;
} }
} }
/// A simple way to read individual bits from an input implementing [Read].
pub struct BitReader<'a, I: Read + ReadBytesExt> {
input: &'a mut I,
current_byte: Option<u8>,
byte_offset: usize,
bit_offset: usize,
}
impl<'a, I: Read + ReadBytesExt> BitReader<'a, I> {
/// Create a new BitReader wrapper around something which
/// implements [Write].
pub fn new(input: &'a mut I) -> Self {
let first = input.read_u8().unwrap();
Self {
input,
current_byte: Some(first),
byte_offset: 0,
bit_offset: 0,
}
}
/// Get the number of whole bytes read from the stream.
pub fn byte_offset(&self) -> usize {
self.byte_offset
}
/// Read some bits from the input.
pub fn read_bit(&mut self, bit_len: usize) -> u64 {
if bit_len > 64 {
panic!("Cannot read more than 64 bits at once.")
} else if bit_len == 0 {
panic!("Must read 1 or more bits.")
}
if bit_len % 8 == 0 && self.bit_offset == 0 {
return self.read(bit_len / 8);
}
let mut result = 0;
for i in 0..bit_len {
let bit_value = ((self.current_byte.unwrap() as usize >> self.bit_offset) & 1) as u64;
self.bit_offset += 1;
if self.bit_offset == 8 {
self.byte_offset += 1;
self.bit_offset = 0;
self.current_byte = Some(self.input.read_u8().unwrap());
}
result |= bit_value << i;
}
result
}
/// Read some bytes from the input.
pub fn read(&mut self, byte_len: usize) -> u64 {
if byte_len > 8 {
panic!("Cannot read more than 8 bytes at once.")
} else if byte_len == 0 {
panic!("Must read 1 or more bytes")
}
let mut padded_slice = vec![0u8; byte_len];
self.input.read_exact(&mut padded_slice).unwrap();
self.byte_offset += byte_len;
let extra_length = padded_slice.len() - byte_len;
padded_slice.extend_from_slice(&vec![0u8; extra_length]);
u64::from_le_bytes(padded_slice.try_into().unwrap())
}
}

View file

@ -1,3 +1,4 @@
use byteorder::ReadBytesExt;
use imagequant::Attributes; use imagequant::Attributes;
use rgb::{ComponentSlice, RGBA8}; use rgb::{ComponentSlice, RGBA8};
use std::{ use std::{
@ -34,7 +35,10 @@ impl Palette {
} }
/// Get a palette from the input stream, beginning where the palette starts. /// Get a palette from the input stream, beginning where the palette starts.
pub fn get_palette<T: Seek + Read>(input: &mut T, num_colors: usize) -> Result<Palette, CzError> { pub fn get_palette<T: Seek + ReadBytesExt + Read>(
input: &mut T,
num_colors: usize,
) -> Result<Palette, CzError> {
let mut colormap = Vec::with_capacity(num_colors); let mut colormap = Vec::with_capacity(num_colors);
let mut rgba_buf = [0u8; 4]; let mut rgba_buf = [0u8; 4];

View file

@ -2,7 +2,7 @@
use std::io::{self, Read, Seek, Write}; use std::io::{self, Read, Seek, Write};
use byteorder_lite::{ReadBytesExt, WriteBytesExt, LE}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use thiserror::Error; use thiserror::Error;
#[derive(Error, Debug)] #[derive(Error, Debug)]
@ -43,7 +43,7 @@ pub enum CzVersion {
} }
impl TryFrom<u8> for CzVersion { impl TryFrom<u8> for CzVersion {
type Error = String; type Error = &'static str;
fn try_from(value: u8) -> Result<Self, Self::Error> { fn try_from(value: u8) -> Result<Self, Self::Error> {
let value = match value { let value = match value {
@ -53,7 +53,7 @@ impl TryFrom<u8> for CzVersion {
3 => Self::CZ3, 3 => Self::CZ3,
4 => Self::CZ4, 4 => Self::CZ4,
5 => Self::CZ5, 5 => Self::CZ5,
v => return Err(format!("{} is not a valid CZ version", v)), _ => return Err("Value is not a valid CZ version"),
}; };
Ok(value) Ok(value)
@ -61,7 +61,7 @@ impl TryFrom<u8> for CzVersion {
} }
impl TryFrom<char> for CzVersion { impl TryFrom<char> for CzVersion {
type Error = String; type Error = &'static str;
fn try_from(value: char) -> Result<Self, Self::Error> { fn try_from(value: char) -> Result<Self, Self::Error> {
let value = match value { let value = match value {
@ -71,7 +71,7 @@ impl TryFrom<char> for CzVersion {
'3' => Self::CZ3, '3' => Self::CZ3,
'4' => Self::CZ4, '4' => Self::CZ4,
'5' => Self::CZ5, '5' => Self::CZ5,
v => return Err(format!("{} is not a valid CZ version", v)), _ => return Err("Value is not a valid CZ version"),
}; };
Ok(value) Ok(value)
@ -132,10 +132,10 @@ impl CommonHeader {
let mut header = Self { let mut header = Self {
version, version,
length: bytes.read_u32::<LE>()?, length: bytes.read_u32::<LittleEndian>()?,
width: bytes.read_u16::<LE>()?, width: bytes.read_u16::<LittleEndian>()?,
height: bytes.read_u16::<LE>()?, height: bytes.read_u16::<LittleEndian>()?,
depth: bytes.read_u16::<LE>()?, depth: bytes.read_u16::<LittleEndian>()?,
unknown: bytes.read_u8()?, unknown: bytes.read_u8()?,
}; };
@ -204,10 +204,10 @@ impl CommonHeader {
let magic_bytes = [b'C', b'Z', b'0' + self.version as u8, b'\0']; let magic_bytes = [b'C', b'Z', b'0' + self.version as u8, b'\0'];
output.write_all(&magic_bytes)?; output.write_all(&magic_bytes)?;
output.write_u32::<LE>(self.length() as u32)?; output.write_u32::<LittleEndian>(self.length() as u32)?;
output.write_u16::<LE>(self.width())?; output.write_u16::<LittleEndian>(self.width())?;
output.write_u16::<LE>(self.height())?; output.write_u16::<LittleEndian>(self.height())?;
output.write_u16::<LE>(self.depth())?; output.write_u16::<LittleEndian>(self.depth())?;
output.write_u8(self.color_block())?; output.write_u8(self.color_block())?;
Ok(()) Ok(())
@ -282,27 +282,27 @@ impl ExtendedHeader {
self self
} }
pub fn from_bytes<T: Seek + Read>( pub fn from_bytes<T: Seek + ReadBytesExt + Read>(
input: &mut T, input: &mut T,
common_header: &CommonHeader, common_header: &CommonHeader,
) -> Result<Self, CzError> { ) -> Result<Self, CzError> {
let mut unknown_1 = [0u8; 5]; let mut unknown_1 = [0u8; 5];
input.read_exact(&mut unknown_1)?; input.read_exact(&mut unknown_1)?;
let crop_width = input.read_u16::<LE>()?; let crop_width = input.read_u16::<LittleEndian>()?;
let crop_height = input.read_u16::<LE>()?; let crop_height = input.read_u16::<LittleEndian>()?;
let bounds_width = input.read_u16::<LE>()?; let bounds_width = input.read_u16::<LittleEndian>()?;
let bounds_height = input.read_u16::<LE>()?; let bounds_height = input.read_u16::<LittleEndian>()?;
let mut offset_width = None; let mut offset_width = None;
let mut offset_height = None; let mut offset_height = None;
let mut unknown_2 = None; let mut unknown_2 = None;
if common_header.length() > 28 { if common_header.length() > 28 {
offset_width = Some(input.read_u16::<LE>()?); offset_width = Some(input.read_u16::<LittleEndian>()?);
offset_height = Some(input.read_u16::<LE>()?); offset_height = Some(input.read_u16::<LittleEndian>()?);
unknown_2 = Some(input.read_u32::<LE>()?); unknown_2 = Some(input.read_u32::<LittleEndian>()?);
} }
Ok(Self { Ok(Self {
@ -321,17 +321,17 @@ impl ExtendedHeader {
}) })
} }
pub fn write_into<T: Write>(&self, output: &mut T) -> Result<(), io::Error> { pub fn write_into<T: WriteBytesExt + Write>(&self, output: &mut T) -> Result<(), io::Error> {
output.write_all(&self.unknown_1)?; output.write_all(&self.unknown_1)?;
output.write_u16::<LE>(self.crop_width)?; output.write_u16::<LittleEndian>(self.crop_width)?;
output.write_u16::<LE>(self.crop_height)?; output.write_u16::<LittleEndian>(self.crop_height)?;
output.write_u16::<LE>(self.bounds_width)?; output.write_u16::<LittleEndian>(self.bounds_width)?;
output.write_u16::<LE>(self.bounds_height)?; output.write_u16::<LittleEndian>(self.bounds_height)?;
if self.offset_width.is_some() { if self.offset_width.is_some() {
output.write_u16::<LE>(self.offset_width.unwrap())?; output.write_u16::<LittleEndian>(self.offset_width.unwrap())?;
output.write_u16::<LE>(self.offset_height.unwrap())?; output.write_u16::<LittleEndian>(self.offset_height.unwrap())?;
output.write_u32::<LE>(self.unknown_2.unwrap())?; output.write_u32::<LittleEndian>(self.unknown_2.unwrap())?;
} }
Ok(()) Ok(())

View file

@ -1,11 +1,11 @@
use byteorder::{ReadBytesExt, WriteBytesExt, LE};
use std::{ use std::{
collections::HashMap, collections::HashMap,
io::{Read, Seek, Write}, io::{Cursor, Read, Seek, Write},
}; };
use crate::binio::BitIo; use crate::binio::{BitReader, BitWriter};
use crate::common::CzError; use crate::common::CzError;
use byteorder_lite::{ReadBytesExt, WriteBytesExt, LE};
/// The size of compressed data in each chunk /// The size of compressed data in each chunk
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -37,7 +37,10 @@ pub struct CompressionInfo {
} }
impl CompressionInfo { impl CompressionInfo {
pub fn write_into<T: Write>(&self, output: &mut T) -> Result<(), std::io::Error> { pub fn write_into<T: WriteBytesExt + Write>(
&self,
output: &mut T,
) -> Result<(), std::io::Error> {
output.write_u32::<LE>(self.chunk_count as u32)?; output.write_u32::<LE>(self.chunk_count as u32)?;
for chunk in &self.chunks { for chunk in &self.chunks {
@ -53,7 +56,9 @@ impl CompressionInfo {
/// ///
/// These are defined by a length value, followed by the number of data chunks /// These are defined by a length value, followed by the number of data chunks
/// that length value says split into compressed and original size u32 values /// that length value says split into compressed and original size u32 values
pub fn get_chunk_info<T: Seek + Read>(bytes: &mut T) -> Result<CompressionInfo, CzError> { pub fn get_chunk_info<T: Seek + ReadBytesExt + Read>(
bytes: &mut T,
) -> Result<CompressionInfo, CzError> {
let parts_count = bytes.read_u32::<LE>()?; let parts_count = bytes.read_u32::<LE>()?;
let mut part_sizes = vec![]; let mut part_sizes = vec![];
@ -84,7 +89,7 @@ pub fn get_chunk_info<T: Seek + Read>(bytes: &mut T) -> Result<CompressionInfo,
} }
/// Decompress an LZW compressed stream like CZ1 /// Decompress an LZW compressed stream like CZ1
pub fn decompress<T: Seek + Read>( pub fn decompress<T: Seek + ReadBytesExt + Read>(
input: &mut T, input: &mut T,
chunk_info: &CompressionInfo, chunk_info: &CompressionInfo,
) -> Result<Vec<u8>, CzError> { ) -> Result<Vec<u8>, CzError> {
@ -139,7 +144,7 @@ fn decompress_lzw(input_data: &[u16], size: usize) -> Vec<u8> {
} }
/// Decompress an LZW compressed stream like CZ2 /// Decompress an LZW compressed stream like CZ2
pub fn decompress2<T: Seek + Read>( pub fn decompress2<T: Seek + ReadBytesExt + Read>(
input: &mut T, input: &mut T,
chunk_info: &CompressionInfo, chunk_info: &CompressionInfo,
) -> Result<Vec<u8>, CzError> { ) -> Result<Vec<u8>, CzError> {
@ -158,7 +163,7 @@ pub fn decompress2<T: Seek + Read>(
} }
fn decompress_lzw2(input_data: &[u8], size: usize) -> Vec<u8> { fn decompress_lzw2(input_data: &[u8], size: usize) -> Vec<u8> {
let data = input_data; let mut data = Cursor::new(input_data);
let mut dictionary = HashMap::new(); let mut dictionary = HashMap::new();
for i in 0..256 { for i in 0..256 {
dictionary.insert(i as u64, vec![i as u8]); dictionary.insert(i as u64, vec![i as u8]);
@ -167,7 +172,7 @@ fn decompress_lzw2(input_data: &[u8], size: usize) -> Vec<u8> {
let mut result = Vec::with_capacity(size); let mut result = Vec::with_capacity(size);
let data_size = input_data.len(); let data_size = input_data.len();
let mut bit_io = BitIo::new(data.to_vec()); let mut bit_io = BitReader::new(&mut data);
let mut w = dictionary.get(&0).unwrap().clone(); let mut w = dictionary.get(&0).unwrap().clone();
let mut element; let mut element;
@ -191,11 +196,7 @@ fn decompress_lzw2(input_data: &[u8], size: usize) -> Vec<u8> {
entry = w.clone(); entry = w.clone();
entry.push(w[0]) entry.push(w[0])
} else { } else {
panic!( panic!("Bad compressed element {} at offset {}", element, bit_io.byte_offset())
"Bad compressed element {} at offset {}",
element,
bit_io.byte_offset()
)
} }
//println!("{}", element); //println!("{}", element);
@ -361,8 +362,9 @@ fn compress_lzw2(data: &[u8], last: Vec<u8>) -> (usize, Vec<u8>, Vec<u8>) {
element = last element = last
} }
let mut bit_io = BitIo::new(vec![0u8; 0xF0000]); let mut output_buf = Vec::new();
let write_bit = |bit_io: &mut BitIo, code: u64| { let mut bit_io = BitWriter::new(&mut output_buf);
let write_bit = |bit_io: &mut BitWriter<Vec<u8>>, code: u64| {
if code > 0x7FFF { if code > 0x7FFF {
bit_io.write_bit(1, 1); bit_io.write_bit(1, 1);
bit_io.write_bit(code, 18); bit_io.write_bit(code, 18);
@ -401,14 +403,17 @@ fn compress_lzw2(data: &[u8], last: Vec<u8>) -> (usize, Vec<u8>, Vec<u8>) {
} }
} }
return (count, bit_io.bytes(), Vec::new()); bit_io.flush().unwrap();
return (count, output_buf, Vec::new());
} else if bit_io.byte_size() < 0x87BDF { } else if bit_io.byte_size() < 0x87BDF {
if !last_element.is_empty() { if !last_element.is_empty() {
write_bit(&mut bit_io, *dictionary.get(&last_element).unwrap()); write_bit(&mut bit_io, *dictionary.get(&last_element).unwrap());
} }
return (count, bit_io.bytes(), Vec::new()); bit_io.flush().unwrap();
return (count, output_buf, Vec::new());
} }
(count, bit_io.bytes(), last_element) bit_io.flush().unwrap();
(count, output_buf, last_element)
} }

View file

@ -1,4 +1,4 @@
use byteorder_lite::ReadBytesExt; use byteorder::ReadBytesExt;
use rgb::ComponentSlice; use rgb::ComponentSlice;
use std::{ use std::{
fs::File, fs::File,
@ -13,7 +13,7 @@ use crate::{
/// A CZ# interface which can open and save any CZ file type. /// A CZ# interface which can open and save any CZ file type.
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct CzFile { pub struct DynamicCz {
header_common: CommonHeader, header_common: CommonHeader,
header_extended: Option<ExtendedHeader>, header_extended: Option<ExtendedHeader>,
@ -24,7 +24,7 @@ pub struct CzFile {
bitmap: Vec<u8>, bitmap: Vec<u8>,
} }
impl CzFile { impl DynamicCz {
/// Decode a CZ# file from anything that implements [`Read`] and [`Seek`] /// Decode a CZ# file from anything that implements [`Read`] and [`Seek`]
/// ///
/// The input must begin with the /// The input must begin with the
@ -192,6 +192,35 @@ impl CzFile {
Ok(()) Ok(())
} }
/// Save the CZ# image as a lossless PNG file.
///
/// Internally, the [`DynamicCz`] struct operates on 32-bit RGBA values,
/// which is the highest encountered in CZ# files, therefore saving them
/// as a PNG of the same or better quality is lossless.
#[cfg(feature = "png")]
pub fn save_as_png<P: ?Sized + AsRef<std::path::Path>>(
&self,
path: &P,
) -> Result<(), image::error::EncodingError> {
let size = (self.header_common.width() as u32 * self.header_common.height() as u32) * 4;
let mut buf = vec![0; size as usize];
buf[..self.bitmap.len()].copy_from_slice(&self.bitmap);
let image = image::RgbaImage::from_raw(
self.header_common.width() as u32,
self.header_common.height() as u32,
buf.clone(),
)
.unwrap();
image
.save_with_format(path, image::ImageFormat::Png)
.unwrap();
Ok(())
}
/// Create a CZ# image from RGBA bytes. The bytes *must* be RGBA, as that /// Create a CZ# image from RGBA bytes. The bytes *must* be RGBA, as that
/// is the only format that is used internally. /// is the only format that is used internally.
pub fn from_raw(version: CzVersion, width: u16, height: u16, bitmap: Vec<u8>) -> Self { pub fn from_raw(version: CzVersion, width: u16, height: u16, bitmap: Vec<u8>) -> Self {

View file

@ -1,8 +1,9 @@
use byteorder::{ReadBytesExt, WriteBytesExt};
use std::io::{Read, Seek, Write}; use std::io::{Read, Seek, Write};
use crate::common::CzError; use crate::common::CzError;
pub fn decode<T: Seek + Read>(input: &mut T) -> Result<Vec<u8>, CzError> { pub fn decode<T: Seek + ReadBytesExt + Read>(input: &mut T) -> Result<Vec<u8>, CzError> {
// Get the rest of the file, which is the bitmap // Get the rest of the file, which is the bitmap
let mut bitmap = vec![]; let mut bitmap = vec![];
input.read_to_end(&mut bitmap)?; input.read_to_end(&mut bitmap)?;
@ -10,7 +11,7 @@ pub fn decode<T: Seek + Read>(input: &mut T) -> Result<Vec<u8>, CzError> {
Ok(bitmap) Ok(bitmap)
} }
pub fn encode<T: Write>(output: &mut T, bitmap: &[u8]) -> Result<(), CzError> { pub fn encode<T: WriteBytesExt + Write>(output: &mut T, bitmap: &[u8]) -> Result<(), CzError> {
output.write_all(bitmap)?; output.write_all(bitmap)?;
Ok(()) Ok(())

View file

@ -1,9 +1,10 @@
use byteorder::{ReadBytesExt, WriteBytesExt};
use std::io::{Read, Seek, SeekFrom, Write}; use std::io::{Read, Seek, SeekFrom, Write};
use crate::common::CzError; use crate::common::CzError;
use crate::compression::{compress, decompress, get_chunk_info}; use crate::compression::{compress, decompress, get_chunk_info};
pub fn decode<T: Seek + Read>(bytes: &mut T) -> Result<Vec<u8>, CzError> { pub fn decode<T: Seek + ReadBytesExt + Read>(bytes: &mut T) -> Result<Vec<u8>, CzError> {
// Get information about the compressed chunks // Get information about the compressed chunks
let block_info = get_chunk_info(bytes)?; let block_info = get_chunk_info(bytes)?;
bytes.seek(SeekFrom::Start(block_info.length as u64))?; bytes.seek(SeekFrom::Start(block_info.length as u64))?;
@ -14,7 +15,7 @@ pub fn decode<T: Seek + Read>(bytes: &mut T) -> Result<Vec<u8>, CzError> {
Ok(bitmap) Ok(bitmap)
} }
pub fn encode<T: Write>(output: &mut T, bitmap: &[u8]) -> Result<(), CzError> { pub fn encode<T: WriteBytesExt + Write>(output: &mut T, bitmap: &[u8]) -> Result<(), CzError> {
let (compressed_data, compressed_info) = compress(bitmap, 0xFEFD); let (compressed_data, compressed_info) = compress(bitmap, 0xFEFD);
compressed_info.write_into(output)?; compressed_info.write_into(output)?;

View file

@ -1,9 +1,10 @@
use byteorder::{ReadBytesExt, WriteBytesExt};
use std::io::{Read, Seek, SeekFrom, Write}; use std::io::{Read, Seek, SeekFrom, Write};
use crate::common::CzError; use crate::common::CzError;
use crate::compression::{compress2, decompress2, get_chunk_info}; use crate::compression::{compress2, decompress2, get_chunk_info};
pub fn decode<T: Seek + Read>(bytes: &mut T) -> Result<Vec<u8>, CzError> { pub fn decode<T: Seek + ReadBytesExt + Read>(bytes: &mut T) -> Result<Vec<u8>, CzError> {
// Get information about the compressed chunks // Get information about the compressed chunks
let block_info = get_chunk_info(bytes)?; let block_info = get_chunk_info(bytes)?;
bytes.seek(SeekFrom::Start(block_info.length as u64))?; bytes.seek(SeekFrom::Start(block_info.length as u64))?;
@ -14,8 +15,8 @@ pub fn decode<T: Seek + Read>(bytes: &mut T) -> Result<Vec<u8>, CzError> {
Ok(bitmap) Ok(bitmap)
} }
pub fn encode<T: Write>(output: &mut T, bitmap: &[u8]) -> Result<(), CzError> { pub fn encode<T: WriteBytesExt + Write>(output: &mut T, bitmap: &[u8]) -> Result<(), CzError> {
let (compressed_data, compressed_info) = compress2(bitmap); let (compressed_data, compressed_info) = compress2(&bitmap);
compressed_info.write_into(output)?; compressed_info.write_into(output)?;

View file

@ -1,9 +1,13 @@
use byteorder::{ReadBytesExt, WriteBytesExt};
use std::io::{Read, Seek, SeekFrom, Write}; use std::io::{Read, Seek, SeekFrom, Write};
use crate::common::{CommonHeader, CzError}; use crate::common::{CommonHeader, CzError};
use crate::compression::{compress, decompress, get_chunk_info}; use crate::compression::{compress, decompress, get_chunk_info};
pub fn decode<T: Seek + Read>(bytes: &mut T, header: &CommonHeader) -> Result<Vec<u8>, CzError> { pub fn decode<T: Seek + ReadBytesExt + Read>(
bytes: &mut T,
header: &CommonHeader,
) -> Result<Vec<u8>, CzError> {
let block_info = get_chunk_info(bytes)?; let block_info = get_chunk_info(bytes)?;
bytes.seek(SeekFrom::Start(block_info.length as u64))?; bytes.seek(SeekFrom::Start(block_info.length as u64))?;
@ -14,7 +18,7 @@ pub fn decode<T: Seek + Read>(bytes: &mut T, header: &CommonHeader) -> Result<Ve
Ok(bitmap) Ok(bitmap)
} }
pub fn encode<T: Write>( pub fn encode<T: WriteBytesExt + Write>(
output: &mut T, output: &mut T,
bitmap: &[u8], bitmap: &[u8],
header: &CommonHeader, header: &CommonHeader,

View file

@ -1,9 +1,13 @@
use byteorder::{ReadBytesExt, WriteBytesExt};
use std::io::{Read, Seek, SeekFrom, Write}; use std::io::{Read, Seek, SeekFrom, Write};
use crate::common::{CommonHeader, CzError}; use crate::common::{CommonHeader, CzError};
use crate::compression::{compress, decompress, get_chunk_info}; use crate::compression::{compress, decompress, get_chunk_info};
pub fn decode<T: Seek + Read>(bytes: &mut T, header: &CommonHeader) -> Result<Vec<u8>, CzError> { pub fn decode<T: Seek + ReadBytesExt + Read>(
bytes: &mut T,
header: &CommonHeader,
) -> Result<Vec<u8>, CzError> {
let block_info = get_chunk_info(bytes)?; let block_info = get_chunk_info(bytes)?;
bytes.seek(SeekFrom::Start(block_info.length as u64))?; bytes.seek(SeekFrom::Start(block_info.length as u64))?;
@ -14,7 +18,7 @@ pub fn decode<T: Seek + Read>(bytes: &mut T, header: &CommonHeader) -> Result<Ve
Ok(bitmap) Ok(bitmap)
} }
pub fn encode<T: Write>( pub fn encode<T: WriteBytesExt + Write>(
output: &mut T, output: &mut T,
bitmap: &[u8], bitmap: &[u8],
header: &CommonHeader, header: &CommonHeader,

View file

@ -17,14 +17,14 @@ use common::CzError;
use std::{io::BufReader, path::Path}; use std::{io::BufReader, path::Path};
/// Open a CZ# file from a path /// Open a CZ# file from a path
pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<CzFile, CzError> { pub fn open<P: ?Sized + AsRef<Path>>(path: &P) -> Result<DynamicCz, CzError> {
let mut img_file = BufReader::new(std::fs::File::open(path)?); let mut img_file = BufReader::new(std::fs::File::open(path)?);
CzFile::decode(&mut img_file) DynamicCz::decode(&mut img_file)
} }
#[doc(inline)] #[doc(inline)]
pub use dynamic::CzFile; pub use dynamic::DynamicCz;
/* /*
#[doc(inline)] #[doc(inline)]

View file

@ -1,6 +1,6 @@
use std::io::Cursor; use std::io::Cursor;
use cz::{common::CzVersion, CzFile}; use cz::{common::CzVersion, DynamicCz};
const KODIM03: (u16, u16, &[u8]) = (128, 128, include_bytes!("test_images/kodim03.rgba")); const KODIM03: (u16, u16, &[u8]) = (128, 128, include_bytes!("test_images/kodim03.rgba"));
const KODIM23: (u16, u16, &[u8]) = (225, 225, include_bytes!("test_images/kodim23.rgba")); const KODIM23: (u16, u16, &[u8]) = (225, 225, include_bytes!("test_images/kodim23.rgba"));
@ -13,13 +13,18 @@ const TEST_IMAGES: &[TestImage] = &[KODIM03, KODIM23, SQPTEXT, DPFLOGO];
#[test] #[test]
fn cz0_round_trip() { fn cz0_round_trip() {
for image in TEST_IMAGES { for image in TEST_IMAGES {
let original_cz = CzFile::from_raw(CzVersion::CZ0, image.0, image.1, image.2.to_vec()); let original_cz = DynamicCz::from_raw(
CzVersion::CZ0,
image.0,
image.1,
image.2.to_vec()
);
let mut cz_bytes = Vec::new(); let mut cz_bytes = Vec::new();
original_cz.encode(&mut cz_bytes).unwrap(); original_cz.encode(&mut cz_bytes).unwrap();
let mut cz_bytes = Cursor::new(cz_bytes); let mut cz_bytes = Cursor::new(cz_bytes);
let decoded_cz = CzFile::decode(&mut cz_bytes).unwrap(); let decoded_cz = DynamicCz::decode(&mut cz_bytes).unwrap();
assert_eq!(original_cz.as_raw(), decoded_cz.as_raw()); assert_eq!(original_cz.as_raw(), decoded_cz.as_raw());
} }
@ -28,13 +33,18 @@ fn cz0_round_trip() {
#[test] #[test]
fn cz1_round_trip() { fn cz1_round_trip() {
for image in TEST_IMAGES { for image in TEST_IMAGES {
let original_cz = CzFile::from_raw(CzVersion::CZ1, image.0, image.1, image.2.to_vec()); let original_cz = DynamicCz::from_raw(
CzVersion::CZ1,
image.0,
image.1,
image.2.to_vec()
);
let mut cz_bytes = Vec::new(); let mut cz_bytes = Vec::new();
original_cz.encode(&mut cz_bytes).unwrap(); original_cz.encode(&mut cz_bytes).unwrap();
let mut cz_bytes = Cursor::new(cz_bytes); let mut cz_bytes = Cursor::new(cz_bytes);
let decoded_cz = CzFile::decode(&mut cz_bytes).unwrap(); let decoded_cz = DynamicCz::decode(&mut cz_bytes).unwrap();
assert_eq!(original_cz.as_raw(), decoded_cz.as_raw()); assert_eq!(original_cz.as_raw(), decoded_cz.as_raw());
} }
@ -42,29 +52,42 @@ fn cz1_round_trip() {
#[test] #[test]
fn cz2_round_trip() { fn cz2_round_trip() {
let mut i = 0;
for image in TEST_IMAGES { for image in TEST_IMAGES {
let original_cz = CzFile::from_raw(CzVersion::CZ2, image.0, image.1, image.2.to_vec()); let original_cz = DynamicCz::from_raw(
CzVersion::CZ2,
image.0,
image.1,
image.2.to_vec()
);
let mut cz_bytes = Vec::new(); let mut cz_bytes = Vec::new();
original_cz.encode(&mut cz_bytes).unwrap(); original_cz.encode(&mut cz_bytes).unwrap();
let mut cz_bytes = Cursor::new(cz_bytes); let mut cz_bytes = Cursor::new(cz_bytes);
let decoded_cz = CzFile::decode(&mut cz_bytes).unwrap(); let decoded_cz = DynamicCz::decode(&mut cz_bytes).unwrap();
assert_eq!(original_cz.as_raw(), decoded_cz.as_raw()); assert_eq!(original_cz.as_raw(), decoded_cz.as_raw());
i += 1;
} }
} }
#[test] #[test]
fn cz3_round_trip() { fn cz3_round_trip() {
for image in TEST_IMAGES { for image in TEST_IMAGES {
let original_cz = CzFile::from_raw(CzVersion::CZ3, image.0, image.1, image.2.to_vec()); let original_cz = DynamicCz::from_raw(
CzVersion::CZ3,
image.0,
image.1,
image.2.to_vec()
);
let mut cz_bytes = Vec::new(); let mut cz_bytes = Vec::new();
original_cz.encode(&mut cz_bytes).unwrap(); original_cz.encode(&mut cz_bytes).unwrap();
let mut cz_bytes = Cursor::new(cz_bytes); let mut cz_bytes = Cursor::new(cz_bytes);
let decoded_cz = CzFile::decode(&mut cz_bytes).unwrap(); let decoded_cz = DynamicCz::decode(&mut cz_bytes).unwrap();
assert_eq!(original_cz.as_raw(), decoded_cz.as_raw()); assert_eq!(original_cz.as_raw(), decoded_cz.as_raw());
} }
@ -73,13 +96,18 @@ fn cz3_round_trip() {
#[test] #[test]
fn cz4_round_trip() { fn cz4_round_trip() {
for image in TEST_IMAGES { for image in TEST_IMAGES {
let original_cz = CzFile::from_raw(CzVersion::CZ4, image.0, image.1, image.2.to_vec()); let original_cz = DynamicCz::from_raw(
CzVersion::CZ4,
image.0,
image.1,
image.2.to_vec()
);
let mut cz_bytes = Vec::new(); let mut cz_bytes = Vec::new();
original_cz.encode(&mut cz_bytes).unwrap(); original_cz.encode(&mut cz_bytes).unwrap();
let mut cz_bytes = Cursor::new(cz_bytes); let mut cz_bytes = Cursor::new(cz_bytes);
let decoded_cz = CzFile::decode(&mut cz_bytes).unwrap(); let decoded_cz = DynamicCz::decode(&mut cz_bytes).unwrap();
assert_eq!(original_cz.as_raw(), decoded_cz.as_raw()); assert_eq!(original_cz.as_raw(), decoded_cz.as_raw());
} }

View file

@ -10,9 +10,9 @@ license = "MIT"
authors.workspace = true authors.workspace = true
[dependencies] [dependencies]
byteorder-lite = "0.1" byteorder = "1.5.0"
log = "0.4" log = "0.4.22"
thiserror = "2.0" thiserror = "1.0.61"
[lints] [lints]
workspace = true workspace = true

View file

@ -58,30 +58,15 @@ impl Entry {
self.length as usize self.length as usize
} }
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Get the raw byte data of an [`Entry`] /// Get the raw byte data of an [`Entry`]
pub fn as_bytes(&self) -> &Vec<u8> { pub fn as_bytes(&self) -> &Vec<u8> {
&self.data &self.data
} }
/// Get the byte data of an entry, but fixed to be compatible with normal things
pub fn cloned_bytes_fixed(&self) -> Vec<u8> {
match self.file_type() {
EntryType::OGGPAK => {
dbg!(self.data[15]);
self.data[15..].to_vec()
},
_ => self.data.clone()
}
}
pub fn display_name(&self) -> String { pub fn display_name(&self) -> String {
let mut name = self.name().clone().unwrap_or(self.id().to_string()); let mut name = self.name().clone().unwrap_or(self.id().to_string());
let entry_type = self.file_type(); let entry_type = self.file_type();
name.push_str(entry_type.extension()); name.push_str(&entry_type.extension());
name name
} }
@ -99,12 +84,6 @@ impl Entry {
} }
} else if self.data[0..3] == [b'M', b'V', b'T'] { } else if self.data[0..3] == [b'M', b'V', b'T'] {
EntryType::MVT EntryType::MVT
} else if self.data[0..4] == [b'R', b'I', b'F', b'F'] {
EntryType::WAV
} else if self.data[0..4] == [b'O', b'g', b'g', b'S'] {
EntryType::OGG
} else if self.data[0..6] == [b'O', b'G', b'G', b'P', b'A', b'K'] {
EntryType::OGGPAK
} else { } else {
EntryType::Unknown EntryType::Unknown
} }
@ -113,7 +92,6 @@ impl Entry {
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryType { pub enum EntryType {
// CZ image files
CZ0, CZ0,
CZ1, CZ1,
CZ2, CZ2,
@ -124,14 +102,6 @@ pub enum EntryType {
/// An MVT video file /// An MVT video file
MVT, MVT,
/// OGG Audio file
OGG,
/// OGGPAK Audio file
OGGPAK,
/// Wav Audio file
WAV,
/// Who knows! /// Who knows!
Unknown, Unknown,
} }
@ -147,9 +117,6 @@ impl EntryType {
Self::CZ4 => ".cz4", Self::CZ4 => ".cz4",
Self::CZ5 => ".cz5", Self::CZ5 => ".cz5",
Self::MVT => ".mvt", Self::MVT => ".mvt",
Self::OGG => ".ogg",
Self::OGGPAK => ".oggpak",
Self::WAV => ".wav",
Self::Unknown => "", Self::Unknown => "",
} }
} }

View file

@ -1,4 +1,4 @@
use byteorder_lite::WriteBytesExt; use byteorder::WriteBytesExt;
use std::io::{self, Write}; use std::io::{self, Write};
use crate::LE; use crate::LE;

View file

@ -1,7 +1,7 @@
pub mod entry; pub mod entry;
pub mod header; pub mod header;
use byteorder_lite::{ReadBytesExt, WriteBytesExt, LE}; use byteorder::{LittleEndian, ReadBytesExt, WriteBytesExt};
use header::Header; use header::Header;
use log::{debug, info}; use log::{debug, info};
use std::{ use std::{
@ -12,6 +12,8 @@ use std::{
}; };
use thiserror::Error; use thiserror::Error;
type LE = LittleEndian;
use crate::{entry::Entry, header::PakFlags}; use crate::{entry::Entry, header::PakFlags};
/// An error associated with a PAK file /// An error associated with a PAK file
@ -64,7 +66,7 @@ pub struct PakLimits {
impl Default for PakLimits { impl Default for PakLimits {
fn default() -> Self { fn default() -> Self {
Self { Self {
entry_limit: 100_000, // 100,000 entries entry_limit: 10_000, // 10,000 entries
size_limit: u32::MAX as usize, // 10 gb size_limit: u32::MAX as usize, // 10 gb
} }
} }

View file

@ -1,14 +0,0 @@
[package]
name = "luca_script"
version = "0.1.0"
edition = "2021"
authors.workspace = true
[dependencies]
byteorder-lite = "0.1.0"
encoding_rs = "0.8.35"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"
[lints]
workspace = true

View file

@ -1,107 +0,0 @@
EQU
EQUN
EQUV
ADD
SUB
MUL
DIV
MOD
AND
OR
RANDOM
VARSTR
SET
FLAGCLR
GOTO
ONGOTO
GOSUB
IFY
IFN
RETURN
JUMP
FARCALL
FARRETURN
JUMPPOINT
END
VARSTR_SET
TALKNAME_SET
ARFLAGSET
COLORBG_SET
SPLINE_SET
SHAKELIST_SET
MESSAGE
MESSAGE_CLEAR
SELECT
CLOSE_WINDOW
LOG
LOG_PAUSE
LOG_END
VOICE
WAIT_COUNT
WAIT_TIME
FFSTOP
INIT
STOP
IMAGELOAD
IMAGEUPADTE
ARC
MOVE
MOVE2
ROT
PEND
FADE
SCALE
SHAKE
SHAKELIST
BASE
MCMOVE
MCARC
MCROT
MCSHAKE
MCFADE
WAIT
DRAW
WIPE
FRAMEON
FRAMEOFF
FW
SCISSOR
DELAY
RASTER
TONE
SCALECOSSIN
BMODE
SIZE
SPLINE
DISP
MASK
SG_QUAKE
BGM
BGM_WAITSTART
BGM_WAITFADE
SE
SE_STOP
SE_WAIT
VOLUME
MOVIE
SETCGFLAG
EX
TROPHY
SETBGMFLAG
TASK
BTFUNC
BATTLE
KOEP
BT_ACCESSORY_SELECT
UNDO_CLEAR
PTFUNC
PT
GMFUNC
GM
DEL_CALLSTACK
FULLQUAKE_ZOOM
LBFUNC
LBBG
HAIKEI_SET
SAYAVOICETEXT
UNKNOWN

View file

@ -1,582 +0,0 @@
mod utils;
use std::{fs, io::{Cursor, Read, Write}, sync::LazyLock};
use byteorder_lite::{WriteBytesExt, ReadBytesExt, LE};
use serde::{Deserialize, Serialize};
use utils::Encoding;
static OPCODES: LazyLock<Vec<String>> = LazyLock::new(|| fs::read_to_string("LBEE_opcodes")
.unwrap()
.split("\n")
.map(|s| s.to_owned())
.collect()
);
fn main() {
let mut script_file = fs::File::open("SEEN0513").unwrap();
let script_len = script_file.metadata().unwrap().len();
let script = decode_script(&mut script_file, script_len);
/*
for c in script.opcodes {
print!("{:>5}", c.position);
print!("{:>12}: ", c.string);
if let Some(o) = c.opcode_specifics {
print!("{}", serde_json::ser::to_string(&o).unwrap());
} else if let Some(r) = c.fixed_param {
print!("{:?}", r);
}
println!();
}
*/
//println!("{}", serde_json::ser::to_string_pretty(&script).unwrap());
let mut rewrite_script = fs::File::create("SEEN0513-rewritten").unwrap();
write_script(&mut rewrite_script, script).unwrap();
println!("Wrote out successfully");
}
fn decode_script<S: Read>(script_stream: &mut S, length: u64) -> Script {
let mut opcodes = Vec::new();
let mut offset = 0;
let mut i = 0;
let mut pos = 0;
while offset < length as usize {
// Read all base info
let length = script_stream.read_u16::<LE>().unwrap() as usize;
let number = script_stream.read_u8().unwrap();
let flag = script_stream.read_u8().unwrap();
let string = OPCODES[number as usize].clone();
offset += 4;
let raw_len = length - 4;
let mut raw_bytes = vec![0u8; raw_len];
script_stream.read_exact(&mut raw_bytes).unwrap();
offset += raw_len;
// Read extra align byte if alignment needed
if length % 2 != 0 {
offset += 1;
Some(script_stream.read_u8().unwrap())
} else {
None
};
let mut fixed_param = Vec::new();
let param_bytes = match flag {
0 => raw_bytes.clone(),
f if f < 2 => {
fixed_param = vec![
u16::from_le_bytes(raw_bytes[..2].try_into().unwrap()),
];
raw_bytes[2..].to_vec()
}
_ => {
fixed_param = vec![
u16::from_le_bytes(raw_bytes[..2].try_into().unwrap()),
u16::from_le_bytes(raw_bytes[2..4].try_into().unwrap()),
];
raw_bytes[4..].to_vec()
}
};
opcodes.push(Opcode {
index: i,
position: pos,
length,
opcode_number: number,
string: string.clone(),
flag,
fixed_param,
opcode_specifics: SpecificOpcode::decode(&string, &param_bytes),
param_bytes,
});
// Break if END opcode reached
if &string == "END" {
break;
}
pos += (length + 1) & !1;
i += 1;
}
Script {
code_count: opcodes.len(),
opcodes,
}
}
fn write_script<W: Write>(script_output: &mut W, script: Script) -> Result<(), ()> {
let mut position = 0;
for opcode in script.opcodes {
let mut total = 0;
script_output.write_u16::<LE>(opcode.length as u16).unwrap();
script_output.write_u8(OPCODES.iter().position(|l| *l == opcode.string).unwrap() as u8).unwrap();
script_output.write_u8(opcode.flag).unwrap();
total += 4;
for p in opcode.fixed_param {
script_output.write_u16::<LE>(p).unwrap();
total += 2;
}
script_output.write_all(&opcode.param_bytes).unwrap();
total += opcode.param_bytes.len();
if (position + total) % 2 != 0 {
script_output.write_u8(0).unwrap();
total += 1;
}
position += total;
}
Ok(())
}
#[derive(Debug, Clone)]
#[derive(Serialize, Deserialize)]
struct Script {
opcodes: Vec<Opcode>,
code_count: usize,
}
#[derive(Debug, Clone)]
#[derive(Serialize, Deserialize)]
struct Opcode {
index: usize,
position: usize,
length: usize,
opcode_number: u8,
string: String,
flag: u8,
fixed_param: Vec<u16>,
param_bytes: Vec<u8>,
opcode_specifics: Option<SpecificOpcode>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
#[derive(Serialize, Deserialize)]
enum SpecificOpcode {
Message {
voice_id: u16,
messages: Vec<String>,
end: Vec<u8>,
},
Add {
var1: u16,
expr: String,
},
EquN {
var1: u16,
value: Option<u16>, //?
},
Select {
var_id: u16,
var0: u16,
var1: u16,
var2: u16,
messages: Vec<String>,
var3: u16,
var4: u16,
var5: u16,
},
_Battle,
Task {
task_type: u16,
var1: Option<u16>,
var2: Option<u16>,
var3: Option<u16>,
var4: Option<u16>,
message_1: Option<Vec<String>>,
message_2: Option<Vec<String>>,
raw_args: Option<Vec<u8>>,
},
SayAVoiceText {
voice_id: u16,
messages: Vec<String>,
},
VarStrSet {
varstr_id: u16,
varstr_str: String,
},
GoTo {
jump_pos: u32,
},
GoSub {
arg1: u16,
jump_pos: u32,
end: Vec<u8>,
},
Jump {
filename: String,
jump_pos: Option<u32>,
},
FarCall {
index: u16,
filename: String,
jump_pos: u32,
end: Vec<u8>,
},
IfN {
condition: String,
jump_pos: u32,
},
IfY {
condition: String,
jump_pos: u32,
},
Random {
var1: u16,
rnd_from: String,
rnd_to: String,
},
ImageLoad {
mode: u16,
image_id: u16,
var1: Option<u16>,
pos_x: Option<u16>,
pos_y: Option<u16>,
end: Vec<u8>,
},
Bgm {
bgm_id: u32,
arg2: Option<u16>,
},
Unknown(Vec<u8>),
}
impl SpecificOpcode {
pub fn decode(opcode_str: &str, param_bytes: &[u8]) -> Option<Self> {
if param_bytes.is_empty() {
return None
}
let mut cursor_param = Cursor::new(param_bytes);
Some(match opcode_str {
"MESSAGE" => Self::decode_message(&mut cursor_param),
"SAYAVOICETEXT" => Self::decode_sayavoicetext(&mut cursor_param),
"SELECT" => Self::decode_select(&mut cursor_param),
"TASK" => Self::decode_task(&mut cursor_param),
"ADD" => Self::decode_add(&mut cursor_param),
"EQUN" => Self::decode_equn(&mut cursor_param),
"RANDOM" => Self::decode_random(&mut cursor_param),
"IFY" => Self::decode_ifn_ify(&mut cursor_param, false),
"IFN" => Self::decode_ifn_ify(&mut cursor_param, true),
"JUMP" => Self::decode_jump(&mut cursor_param),
"GOTO" => Self::decode_goto(&mut cursor_param),
"GOSUB" => Self::decode_gosub(&mut cursor_param),
"FARCALL" => Self::decode_farcall(&mut cursor_param),
"VARSTR_SET" => Self::decode_varstr_set(&mut cursor_param),
"IMAGELOAD" => Self::decode_imageload(&mut cursor_param),
"BGM" => Self::decode_bgm(&mut cursor_param),
_ => Self::Unknown(param_bytes.to_vec())
})
}
fn decode_message<R: Read>(param_bytes: &mut R) -> Self {
let voice_id = param_bytes.read_u16::<LE>().unwrap();
// TODO: This will need to change per-game based on the number of
// languages and their encodings
let mut messages = Vec::new();
for _ in 0..2 {
let string = utils::decode_string_v1(param_bytes, Encoding::UTF16).unwrap();
messages.push(string);
}
let mut end = Vec::new();
param_bytes.read_to_end(&mut end).unwrap();
Self::Message {
voice_id,
messages,
end,
}
}
fn decode_add<R: Read>(param_bytes: &mut R) -> Self {
let var1 = param_bytes.read_u16::<LE>().unwrap();
let expr = utils::decode_string_v1(param_bytes, Encoding::ShiftJIS).unwrap();
Self::Add { var1, expr }
}
fn decode_equn<R: Read>(param_bytes: &mut R) -> Self {
let var1 = param_bytes.read_u16::<LE>().unwrap();
let value = param_bytes.read_u16::<LE>().ok();
Self::EquN { var1, value }
}
fn decode_select<R: Read>(param_bytes: &mut R) -> Self {
let var_id = param_bytes.read_u16::<LE>().unwrap();
let var0 = param_bytes.read_u16::<LE>().unwrap();
let var1 = param_bytes.read_u16::<LE>().unwrap();
let var2 = param_bytes.read_u16::<LE>().unwrap();
// TODO: This will need to change per-game based on the number of
// languages and their encodings
let mut messages = Vec::new();
for _ in 0..2 {
let string = utils::decode_string_v1(param_bytes, Encoding::UTF16).unwrap();
messages.push(string);
}
let var3 = param_bytes.read_u16::<LE>().unwrap();
let var4 = param_bytes.read_u16::<LE>().unwrap();
let var5 = param_bytes.read_u16::<LE>().unwrap();
Self::Select {
var_id,
var0,
var1,
var2,
messages,
var3,
var4,
var5
}
}
fn decode_random<R: Read>(param_bytes: &mut R) -> Self {
let var1 = param_bytes.read_u16::<LE>().unwrap();
let rnd_from = utils::decode_string_v1(param_bytes, Encoding::ShiftJIS).unwrap();
let rnd_to = utils::decode_string_v1(param_bytes, Encoding::ShiftJIS).unwrap();
Self::Random { var1, rnd_from, rnd_to }
}
fn decode_ifn_ify<R: Read>(param_bytes: &mut R, ifn: bool) -> Self {
let condition = utils::decode_string_v1(param_bytes, Encoding::ShiftJIS).unwrap();
let jump_pos = param_bytes.read_u32::<LE>().unwrap();
if ifn {
Self::IfN { condition, jump_pos }
} else {
Self::IfY { condition, jump_pos }
}
}
fn decode_jump<R: Read>(param_bytes: &mut R) -> Self {
let filename = utils::decode_string_v1(param_bytes, Encoding::ShiftJIS).unwrap();
let jump_pos = param_bytes.read_u32::<LE>().ok();
Self::Jump { filename, jump_pos }
}
fn decode_imageload<R: Read>(param_bytes: &mut R) -> Self {
let mode = param_bytes.read_u16::<LE>().unwrap();
let image_id = param_bytes.read_u16::<LE>().unwrap();
// These will only be read if there is anything to be read
let var1 = param_bytes.read_u16::<LE>().ok();
let pos_x = param_bytes.read_u16::<LE>().ok();
let pos_y = param_bytes.read_u16::<LE>().ok();
let mut end = Vec::new();
param_bytes.read_to_end(&mut end).unwrap();
Self::ImageLoad {
mode,
image_id,
var1,
pos_x,
pos_y,
end,
}
}
fn decode_goto<R: Read>(param_bytes: &mut R) -> Self {
let jump_pos = param_bytes.read_u32::<LE>().unwrap();
Self::GoTo { jump_pos }
}
fn decode_gosub<R: Read>(param_bytes: &mut R) -> Self {
let arg1 = param_bytes.read_u16::<LE>().unwrap();
let jump_pos = param_bytes.read_u32::<LE>().unwrap();
let mut end = Vec::new();
param_bytes.read_to_end(&mut end).unwrap();
Self::GoSub {
arg1,
jump_pos,
end,
}
}
fn decode_varstr_set<R: Read>(param_bytes: &mut R) -> Self {
let varstr_id = param_bytes.read_u16::<LE>().unwrap();
let varstr_str = utils::decode_string_v1(param_bytes, Encoding::ShiftJIS).unwrap();
Self::VarStrSet { varstr_id, varstr_str }
}
fn decode_farcall<R: Read>(param_bytes: &mut R) -> Self {
let index = param_bytes.read_u16::<LE>().unwrap();
let filename = utils::decode_string_v1(param_bytes, Encoding::ShiftJIS).unwrap();
let jump_pos = param_bytes.read_u32::<LE>().unwrap();
let mut end = Vec::new();
param_bytes.read_to_end(&mut end).unwrap();
Self::FarCall {
index,
filename,
jump_pos,
end,
}
}
fn decode_sayavoicetext<R: Read>(param_bytes: &mut R) -> Self {
let voice_id = param_bytes.read_u16::<LE>().unwrap();
// TODO: This will need to change per-game based on the number of
// languages and their encodings
let mut messages = Vec::new();
for _ in 0..2 {
let string = utils::decode_string_v1(param_bytes, Encoding::UTF16).unwrap();
messages.push(string);
}
Self::SayAVoiceText {
voice_id,
messages,
}
}
fn decode_bgm<R: Read>(param_bytes: &mut R) -> Self {
// TODO: invesigate the accuracy of this
let bgm_id = param_bytes.read_u32::<LE>().unwrap();
let arg2 = param_bytes.read_u16::<LE>().ok();
Self::Bgm {
bgm_id,
arg2,
}
}
fn decode_task<R: Read>(param_bytes: &mut R) -> Self {
let task_type = param_bytes.read_u16::<LE>().unwrap();
let mut var1 = None;
let mut var2 = None;
let mut var3 = None;
let mut var4 = None;
let mut message_1 = None;
let mut message_2 = None;
let raw_args: Option<Vec<u8>> = None;
if false {
return Self::Task { task_type, var1, var2, var3, var4, message_1, message_2, raw_args };
}
match task_type {
4 => {
let var1 = param_bytes.read_u16::<LE>().ok();
if false {
return Self::Task { task_type, var1, var2, var3, var4, message_1, message_2, raw_args };
}
if [0, 4, 5].contains(&var1.unwrap()) {
var2 = param_bytes.read_u16::<LE>().ok();
let mut messages = Vec::new();
for _ in 0..2 {
let string = utils::decode_string_v1(param_bytes, Encoding::UTF16).unwrap();
messages.push(string);
}
message_1 = Some(messages);
} else if var1.unwrap() == 1 {
var2 = param_bytes.read_u16::<LE>().ok();
var3 = param_bytes.read_u16::<LE>().ok();
var4 = param_bytes.read_u16::<LE>().ok();
// Get first set of messages
let mut messages = Vec::new();
for _ in 0..2 {
let string = utils::decode_string_v1(param_bytes, Encoding::UTF16).unwrap();
messages.push(string);
}
message_1 = Some(messages);
// Get second set of messages
let mut messages = Vec::new();
for _ in 0..2 {
let string = utils::decode_string_v1(param_bytes, Encoding::UTF16).unwrap();
messages.push(string);
}
message_2 = Some(messages);
} else if var1.unwrap() == 6 {
var2 = param_bytes.read_u16::<LE>().ok();
var3 = param_bytes.read_u16::<LE>().ok();
let mut messages = Vec::new();
for _ in 0..2 {
let string = utils::decode_string_v1(param_bytes, Encoding::UTF16).unwrap();
messages.push(string);
}
message_1 = Some(messages);
} else {
return Self::Task { task_type, var1, var2, var3, var4, message_1, message_2, raw_args };
}
}
54 => {
let string = utils::decode_string_v1(param_bytes, Encoding::UTF16).unwrap();
message_1 = Some(vec![string]);
}
69 => {
var1 = param_bytes.read_u16::<LE>().ok();
// Get first set of messages
let mut messages = Vec::new();
for _ in 0..2 {
let string = utils::decode_string_v1(param_bytes, Encoding::UTF16).unwrap();
messages.push(string);
}
message_1 = Some(messages);
// Get second set of messages
let mut messages = Vec::new();
for _ in 0..2 {
let string = utils::decode_string_v1(param_bytes, Encoding::UTF16).unwrap();
messages.push(string);
}
message_2 = Some(messages);
}
_ => return Self::Task {
task_type,
var1,
var2,
var3,
var4,
message_1,
message_2,
raw_args,
}
}
Self::Task {
task_type,
var1,
var2,
var3,
var4,
message_1,
message_2,
raw_args,
}
}
}

View file

@ -1,70 +0,0 @@
use std::{error::Error, io::{Read, Write}};
use encoding_rs::*;
use byteorder_lite::{LE, ReadBytesExt};
pub enum Encoding {
#[allow(dead_code)]
UTF8,
UTF16,
ShiftJIS,
}
#[allow(dead_code)]
impl Encoding {
pub fn width(&self) -> usize {
match self {
Self::UTF8 | Self::ShiftJIS => 1,
Self::UTF16 => 2,
}
}
}
pub fn decode_string_v1<R: Read>(
input: &mut R,
format: Encoding,
) -> Result<String, Box<dyn Error>> {
// Find the end of the string
let mut string_buf = Vec::new();
match format {
Encoding::UTF8 | Encoding::ShiftJIS => {
let mut string_byte = input.read_u8()?;
while string_byte != 0 {
string_buf.push(string_byte);
string_byte = input.read_u8()?;
}
},
Encoding::UTF16 => {
let mut string_u16 = input.read_u16::<LE>()?;
while string_u16 != 0 {
string_buf.write_all(&string_u16.to_le_bytes()).unwrap();
string_u16 = input.read_u16::<LE>()?;
}
},
}
// Get the actual string data using the proper decoder
let string = match format {
Encoding::UTF8 => String::from_utf8(string_buf)?,
Encoding::UTF16 => {
String::from_utf16(
&string_buf.chunks_exact(2)
.map(|e| u16::from_le_bytes(e.try_into().unwrap()))
.collect::<Vec<u16>>()
)?
}
Encoding::ShiftJIS => SHIFT_JIS.decode(&string_buf).0.to_string(),
};
Ok(string)
}
#[allow(dead_code)]
pub fn encode_string_v1(string: String, format: Encoding) -> Vec<u8> {
match format {
Encoding::UTF8 => string.as_bytes().to_vec(),
Encoding::UTF16 => string.encode_utf16().flat_map(|b| b.to_le_bytes()).collect(),
Encoding::ShiftJIS => SHIFT_JIS.encode(&string).0.to_vec(),
}
}

View file

@ -1,7 +1,7 @@
[package] [package]
name = "pak_explorer" name = "pak_explorer"
edition = "2024" edition = "2021"
version = "0.1.3" version = "0.1.2"
description = """ description = """
A simple GUI for exploring and making modifications to LUCA System PAK files. A simple GUI for exploring and making modifications to LUCA System PAK files.
""" """
@ -10,16 +10,13 @@ authors.workspace = true
publish = false publish = false
[dependencies] [dependencies]
colog = "1.3" colog = "1.3.0"
cz = { path = "../cz/" } cz = { path = "../cz/", features = ["png"] }
eframe = { version = "0.29", default-features = false, features = ["wayland", "x11", "accesskit", "default_fonts", "wgpu"] } eframe = { version = "0.28.1", default-features = false, features = ["wayland", "x11", "accesskit", "default_fonts", "wgpu"] }
egui_extras = "0.29" egui_extras = "0.28.1"
image = { version = "0.25", default-features = false, features = ["png"] } log = "0.4.22"
kira = "0.10"
log = "0.4"
luca_pak = { path = "../luca_pak/" } luca_pak = { path = "../luca_pak/" }
rfd = "0.15" rfd = "0.14.1"
symphonia = "0.5.4"
[lints] [lints]
workspace = true workspace = true

View file

@ -1,12 +1,10 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use eframe::egui::{ use colog;
self, ColorImage, Image, ProgressBar, TextureFilter, TextureHandle, TextureOptions, ThemePreference use eframe::egui::{self, ColorImage, Image, TextureFilter, TextureHandle, TextureOptions};
};
use kira::{sound::static_sound::{StaticSoundData, StaticSoundHandle}, AudioManager, AudioManagerSettings, DefaultBackend, Tween};
use log::error; use log::error;
use luca_pak::{entry::EntryType, Pak}; use luca_pak::{entry::EntryType, Pak};
use std::{fs, io::Cursor, time::Duration}; use std::fs;
fn main() -> eframe::Result { fn main() -> eframe::Result {
colog::default_builder() colog::default_builder()
@ -15,26 +13,17 @@ fn main() -> eframe::Result {
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([1024.0, 800.0]), viewport: egui::ViewportBuilder::default().with_inner_size([1024.0, 800.0]),
follow_system_theme: true,
..Default::default() ..Default::default()
}; };
let manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default()).unwrap();
eframe::run_native( eframe::run_native(
"LUCA PAK Explorer", "LUCA PAK Explorer",
options, options,
Box::new(|ctx| { Box::new(|ctx| {
let ppp = ctx.egui_ctx.pixels_per_point() * 1.5; let ppp = ctx.egui_ctx.pixels_per_point() * 1.5;
ctx.egui_ctx.set_pixels_per_point(ppp); ctx.egui_ctx.set_pixels_per_point(ppp);
Ok(Box::new(PakExplorer { Ok(Box::<PakExplorer>::default())
open_file: None,
selected_entry: None,
image_texture: None,
hex_string: None,
audio_player: manager,
audio_handle: None,
audio_duration: None,
}))
}), }),
) )
} }
@ -42,18 +31,27 @@ fn main() -> eframe::Result {
struct PakExplorer { struct PakExplorer {
open_file: Option<Pak>, open_file: Option<Pak>,
selected_entry: Option<luca_pak::entry::Entry>, selected_entry: Option<luca_pak::entry::Entry>,
entry_text: String,
image_texture: Option<egui::TextureHandle>, image_texture: Option<egui::TextureHandle>,
hex_string: Option<Vec<String>>, hex_string: Option<Vec<String>>,
audio_player: AudioManager, }
audio_handle: Option<StaticSoundHandle>,
audio_duration: Option<Duration>, impl Default for PakExplorer {
fn default() -> Self {
Self {
open_file: None,
selected_entry: None,
image_texture: None,
hex_string: None,
entry_text: String::new(),
}
}
} }
impl eframe::App for PakExplorer { impl eframe::App for PakExplorer {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| { egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("PAK File Explorer"); ui.heading("PAK File Explorer");
ctx.options_mut(|o| o.theme_preference = ThemePreference::System);
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("Open file").clicked() { if ui.button("Open file").clicked() {
@ -69,13 +67,6 @@ impl eframe::App for PakExplorer {
self.selected_entry = None; self.selected_entry = None;
self.image_texture = None; self.image_texture = None;
self.hex_string = None; self.hex_string = None;
if let Some(a) = self.audio_handle.as_mut() {
a.stop(Tween::default());
}
self.audio_handle = None;
self.audio_duration = None;
} }
} }
if let Some(pak) = &self.open_file { if let Some(pak) = &self.open_file {
@ -106,7 +97,7 @@ impl eframe::App for PakExplorer {
}; };
ui.horizontal(|ui| { ui.horizontal(|ui| {
egui::ComboBox::from_id_salt("my-combobox") egui::ComboBox::from_id_source("my-combobox")
.selected_text(selection.clone()) .selected_text(selection.clone())
.truncate() .truncate()
.show_ui(ui, |ui| { .show_ui(ui, |ui| {
@ -121,13 +112,6 @@ impl eframe::App for PakExplorer {
.clicked() .clicked()
{ {
self.image_texture = None; self.image_texture = None;
if let Some(a) = self.audio_handle.as_mut() {
a.stop(Tween::default());
}
self.audio_handle = None;
self.audio_duration = None;
}; };
} }
}); });
@ -174,18 +158,11 @@ impl eframe::App for PakExplorer {
.set_file_name(display_name) .set_file_name(display_name)
.save_file() .save_file()
{ {
let cz = let cz = cz::DynamicCz::decode(&mut std::io::Cursor::new(
cz::CzFile::decode(&mut std::io::Cursor::new(entry.as_bytes())) entry.as_bytes(),
.unwrap(); ))
image::save_buffer_with_format(
path,
cz.as_raw(),
cz.header().width() as u32,
cz.header().height() as u32,
image::ColorType::Rgba8,
image::ImageFormat::Png,
)
.unwrap(); .unwrap();
cz.save_as_png(&path).unwrap();
} }
} }
@ -193,7 +170,7 @@ impl eframe::App for PakExplorer {
let texture: &TextureHandle = self.image_texture.get_or_insert_with(|| { let texture: &TextureHandle = self.image_texture.get_or_insert_with(|| {
let cz = let cz =
cz::CzFile::decode(&mut std::io::Cursor::new(entry.as_bytes())) cz::DynamicCz::decode(&mut std::io::Cursor::new(entry.as_bytes()))
.unwrap(); .unwrap();
let image = ColorImage::from_rgba_unmultiplied( let image = ColorImage::from_rgba_unmultiplied(
[cz.header().width() as usize, cz.header().height() as usize], [cz.header().width() as usize, cz.header().height() as usize],
@ -219,46 +196,6 @@ impl eframe::App for PakExplorer {
) )
}); });
} }
EntryType::OGG
| EntryType::OGGPAK
| EntryType::WAV => {
ui.separator();
ui.horizontal(|ui| {
if ui.button("").clicked() && self.audio_handle.is_none() {
let sound_data = StaticSoundData::from_cursor(
Cursor::new(entry.cloned_bytes_fixed())
)
.unwrap()
.volume(-8.0);
self.audio_duration = Some(sound_data.duration());
self.audio_handle = Some(self.audio_player.play(sound_data.clone()).unwrap());
}
if ui.button("").clicked() && self.audio_handle.is_some() {
self.audio_handle.as_mut().unwrap().stop(Tween::default());
self.audio_handle = None;
self.audio_duration = None;
}
if let Some(a) = &self.audio_handle {
let pos = a.position() as f32;
ui.add(ProgressBar::new(
pos / self.audio_duration.as_ref().unwrap().as_secs_f32()
).rounding(1.0).text(format!("{:02.0}:{:02.0}", pos / 60.0, pos % 60.0)));
if pos / self.audio_duration.as_ref().unwrap().as_secs_f32() > 0.99 {
self.audio_handle.as_mut().unwrap().stop(Tween::default());
self.audio_handle = None;
self.audio_duration = None;
}
}
ctx.request_repaint_after(Duration::from_millis(50));
});
}
_ => { _ => {
ui.centered_and_justified(|ui| ui.label("No Preview Available")); ui.centered_and_justified(|ui| ui.label("No Preview Available"));
} }

View file

@ -1,10 +1,9 @@
[package] [package]
name = "lbee-utils" name = "utils"
version = "0.2.1" version = "0.1.1"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
authors.workspace = true authors.workspace = true
build = "build.rs"
[[bin]] [[bin]]
name = "czutil" name = "czutil"
@ -13,14 +12,11 @@ name = "czutil"
name = "pakutil" name = "pakutil"
[dependencies] [dependencies]
cz = { path = "../cz/" } cz = { path = "../cz/", features = ["png"] }
luca_pak = { path = "../luca_pak/" } luca_pak = { path = "../luca_pak/" }
image = { version = "0.25", default-features = false, features = ["png"] }
clap = { version = "4.5", features = ["derive", "error-context"] }
owo-colors = "4.1"
[build-dependencies] image = { version = "0.25", default-features = false, features = ["png"] }
vergen-gix = { version = "1.0", features = ["build", "cargo", "rustc", "si"] } clap = { version = "4.5.9", features = ["derive"] }
[lints] [lints]
workspace = true workspace = true

View file

@ -1,23 +0,0 @@
use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder, RustcBuilder, SysinfoBuilder};
fn main() {
let build = BuildBuilder::all_build().unwrap();
let cargo = CargoBuilder::all_cargo().unwrap();
let gitcl = GixBuilder::all_git().unwrap();
let rustc = RustcBuilder::all_rustc().unwrap();
let si = SysinfoBuilder::all_sysinfo().unwrap();
Emitter::default()
.add_instructions(&build)
.unwrap()
.add_instructions(&cargo)
.unwrap()
.add_instructions(&gitcl)
.unwrap()
.add_instructions(&rustc)
.unwrap()
.add_instructions(&si)
.unwrap()
.emit()
.unwrap();
}

View file

@ -1,32 +1,22 @@
use clap::{error::ErrorKind, Error, Parser, Subcommand}; use clap::{error::ErrorKind, Error, Parser, Subcommand};
use cz::{common::CzVersion, CzFile};
use image::ColorType;
use lbee_utils::version;
use owo_colors::OwoColorize;
use std::{ use std::{
fs, fs,
path::{Path, PathBuf}, path::{Path, PathBuf},
process::exit,
}; };
/// Utility to maniuplate CZ image files from the LUCA System game engine by /// Utility to maniuplate CZ image files from the LUCA System game engine by
/// Prototype Ltd. /// Prototype Ltd.
#[derive(Parser)] #[derive(Parser)]
#[command(name = "CZ Utility")] #[command(name = "CZ Utility")]
#[command(author, version, about, long_about = None, disable_version_flag = true)] #[command(version, about, long_about = None)]
#[command(arg_required_else_help(true))]
struct Cli { struct Cli {
/// Show program version information
#[arg(short('V'), long)]
version: bool,
#[command(subcommand)] #[command(subcommand)]
command: Option<Commands>, command: Commands,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
enum Commands { enum Commands {
/// Decode a CZ file to a PNG /// Converts a CZ file to a PNG
Decode { Decode {
/// Decode a whole folder, and output to another folder /// Decode a whole folder, and output to another folder
#[arg(short, long)] #[arg(short, long)]
@ -41,26 +31,7 @@ enum Commands {
output: Option<PathBuf>, output: Option<PathBuf>,
}, },
/// Encode a PNG file to a CZ /// Replace a CZ file's image data
Encode {
/// Input image to encode
#[arg(value_name = "INPUT")]
input: PathBuf,
/// Output CZ file location
#[arg(value_name = "OUTPUT")]
output: PathBuf,
/// Output CZ file version
#[arg(short, long, value_name = "CZ VERSION")]
version: Option<u8>,
/// Output CZ file bit depth
#[arg(short, long, value_name = "CZ BIT DEPTH")]
depth: Option<u16>,
},
/// Replace an existing CZ file's image data
Replace { Replace {
/// Replace a whole folder, and output to another folder, /// Replace a whole folder, and output to another folder,
/// using a folder of replacements /// using a folder of replacements
@ -92,37 +63,36 @@ enum Commands {
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
if cli.version {
println!("{}", version(env!("CARGO_BIN_NAME")));
exit(0);
}
let command = match cli.command {
Some(c) => c,
None => exit(0),
};
// Check what subcommand was run // Check what subcommand was run
match &command { match &cli.command {
Commands::Decode { Commands::Decode {
input, input,
output, output,
batch, batch,
} => { } => {
if !input.exists() { if !input.exists() {
pretty_error("The input file/folder provided does not exist"); Error::raw(
exit(1); ErrorKind::ValueValidation,
"The input file/folder provided does not exist\n",
)
.exit()
} }
if *batch { if *batch {
if input.is_file() { if input.is_file() {
pretty_error("Batch input must be a directory"); Error::raw(
exit(1); ErrorKind::ValueValidation,
"Batch input must be a directory\n",
)
.exit()
} }
if output.is_none() || output.as_ref().unwrap().is_file() { if output.is_none() || output.as_ref().unwrap().is_file() {
pretty_error("Batch output must be a directory"); Error::raw(
exit(1); ErrorKind::ValueValidation,
"Batch output must be a directory\n",
)
.exit()
} }
for entry in fs::read_dir(input).unwrap() { for entry in fs::read_dir(input).unwrap() {
@ -140,48 +110,29 @@ fn main() {
let cz = match cz::open(&path) { let cz = match cz::open(&path) {
Ok(cz) => cz, Ok(cz) => cz,
Err(_) => { Err(_) => {
pretty_error(&format!( Error::raw(
ErrorKind::ValueValidation,
format!(
"Could not open input as a CZ file: {}\n", "Could not open input as a CZ file: {}\n",
path.into_os_string().to_str().unwrap() path.into_os_string().to_str().unwrap()
)); ),
)
.print()
.unwrap();
continue; continue;
} }
}; };
image::save_buffer_with_format( cz.save_as_png(&final_path).unwrap();
final_path,
cz.as_raw(),
cz.header().width() as u32,
cz.header().height() as u32,
ColorType::Rgba8,
image::ImageFormat::Png,
)
.unwrap();
} }
} else { } else {
let cz = cz::open(input).unwrap(); let cz = cz::open(input).unwrap();
if let Some(output) = output { if let Some(output) = output {
image::save_buffer_with_format( cz.save_as_png(output).unwrap();
output,
cz.as_raw(),
cz.header().width() as u32,
cz.header().height() as u32,
ColorType::Rgba8,
image::ImageFormat::Png,
)
.unwrap();
} else { } else {
let file_stem = PathBuf::from(input.file_name().unwrap()); let file_stem = PathBuf::from(input.file_name().unwrap());
image::save_buffer_with_format( cz.save_as_png(&file_stem.with_extension("png")).unwrap();
file_stem.with_extension("png"),
cz.as_raw(),
cz.header().width() as u32,
cz.header().height() as u32,
ColorType::Rgba8,
image::ImageFormat::Png,
)
.unwrap();
} }
} }
} }
@ -194,30 +145,45 @@ fn main() {
depth, depth,
} => { } => {
if !input.exists() { if !input.exists() {
pretty_error("The input file does not exist"); Error::raw(
exit(1); ErrorKind::ValueValidation,
"The original file provided does not exist\n",
)
.exit()
} }
if !replacement.exists() { if !replacement.exists() {
pretty_error("The replacement file does not exist"); Error::raw(
exit(1); ErrorKind::ValueValidation,
"The replacement file provided does not exist\n",
)
.exit()
} }
// If it's a batch replacement, we want directories to search // If it's a batch replacement, we want directories to search
if *batch { if *batch {
if !input.is_dir() { if !input.is_dir() {
pretty_error("Batch input must be a directory"); Error::raw(
exit(1); ErrorKind::ValueValidation,
"Batch input location must be a directory\n",
)
.exit()
} }
if !replacement.is_dir() { if !replacement.is_dir() {
pretty_error("Batch replacement must be a directory"); Error::raw(
exit(1); ErrorKind::ValueValidation,
"Batch replacement location must be a directory\n",
)
.exit()
} }
if !output.is_dir() { if !output.is_dir() {
pretty_error("Batch output location must be a directory"); Error::raw(
exit(1); ErrorKind::ValueValidation,
"Batch output location must be a directory\n",
)
.exit()
} }
// Replace all the files within the directory and print errors for them // Replace all the files within the directory and print errors for them
@ -249,89 +215,17 @@ fn main() {
} }
} else { } else {
if !input.is_file() { if !input.is_file() {
pretty_error("Input must be a file"); Error::raw(ErrorKind::ValueValidation, "Input must be a file\n").exit()
exit(1);
} }
if !replacement.is_file() { if !replacement.is_file() {
pretty_error("Replacement must be a file"); Error::raw(ErrorKind::ValueValidation, "Replacement must be a file\n").exit()
exit(1);
} }
// Replace the input file with the new image // Replace the input file with the new image
replace_cz(&input, &output, &replacement, version, depth).unwrap(); replace_cz(&input, &output, &replacement, version, depth).unwrap();
} }
} }
Commands::Encode {
input,
output,
version,
depth,
} => {
if !input.exists() {
pretty_error("The original file provided does not exist");
exit(1);
}
let version = if let Some(v) = version {
match CzVersion::try_from(*v) {
Ok(v) => v,
Err(_) => {
pretty_error(&format!(
"Invalid CZ version {}; must be 0, 1, 2, 3, or 4",
v
));
exit(1);
}
}
} else if output
.extension()
.is_some_and(|e| e.to_ascii_lowercase().to_string_lossy().starts_with("cz"))
{
let ext_string = output.extension().unwrap().to_string_lossy();
let last_char = ext_string.chars().last().unwrap();
match CzVersion::try_from(last_char) {
Ok(v) => v,
Err(e) => {
pretty_error(&format!("Invalid CZ type: {}", e));
exit(1);
}
}
} else {
pretty_error("CZ version not specified or not parseable from file path");
exit(1);
};
let image = match image::open(input) {
Ok(i) => i,
Err(e) => {
pretty_error(&format!("Could not open input file: {e}"));
exit(1);
}
};
let image_depth = image.color();
let mut cz = CzFile::from_raw(
version,
image.width() as u16,
image.height() as u16,
image.to_rgba8().into_vec(),
);
if let Some(d) = *depth {
if !(d == 8 || d == 24 || d == 32) {
pretty_error(&format!(
"The color depth provided is not valid. Choose from: {}",
"8, 24, or 32".bright_magenta()
));
exit(1);
}
cz.header_mut().set_depth(d);
} else {
cz.header_mut().set_depth(image_depth.bits_per_pixel());
}
cz.save_as_cz(output).expect("Saving CZ file failed");
}
} }
} }
@ -353,7 +247,7 @@ fn replace_cz<P: ?Sized + AsRef<Path>>(
} }
// Open the replacement image and convert it to RGBA8 // Open the replacement image and convert it to RGBA8
let repl_img = image::open(replacement_path)?.to_rgba8(); let repl_img = image::open(&replacement_path)?.to_rgba8();
// Open the original CZ file // Open the original CZ file
let mut cz = cz::open(&path)?; let mut cz = cz::open(&path)?;
@ -377,7 +271,3 @@ fn replace_cz<P: ?Sized + AsRef<Path>>(
Ok(()) Ok(())
} }
fn pretty_error(message: &str) {
eprintln!("{}: {}", "Error".red().italic(), message);
}

View file

@ -2,26 +2,20 @@ use clap::{
error::{Error, ErrorKind}, error::{Error, ErrorKind},
Parser, Subcommand, Parser, Subcommand,
}; };
use lbee_utils::version;
use luca_pak::Pak; use luca_pak::Pak;
use std::{fs, path::PathBuf, process::exit}; use std::{fs, path::PathBuf};
/// Utility to maniuplate PAK archive files from the LUCA System game engine by /// Utility to maniuplate PAK archive files from the LUCA System game engine by
/// Prototype Ltd. /// Prototype Ltd.
#[derive(Parser)] #[derive(Parser)]
#[command(name = "PAK Utility")] #[command(name = "PAK Utility")]
#[command(author, version, about, long_about = None, disable_version_flag = true)] #[command(version, about, long_about = None)]
#[command(arg_required_else_help(true))]
struct Cli { struct Cli {
/// Show program version information #[arg(value_name = "PAK FILE")]
#[arg(short('V'), long)] input: PathBuf,
version: bool,
#[arg(value_name = "PAK FILE", required_unless_present("version"))]
input: Option<PathBuf>,
#[command(subcommand)] #[command(subcommand)]
command: Option<Commands>, command: Commands,
} }
#[derive(Subcommand)] #[derive(Subcommand)]
@ -65,24 +59,12 @@ enum Commands {
fn main() { fn main() {
let cli = Cli::parse(); let cli = Cli::parse();
if cli.version { let mut pak = match Pak::open(&cli.input) {
println!("{}", version(env!("CARGO_BIN_NAME")));
exit(0);
}
let mut pak = match Pak::open(&cli.input.unwrap()) {
Ok(pak) => pak, Ok(pak) => pak,
Err(err) => fmt_error(&format!("Could not open PAK file: {}", err)).exit(), Err(err) => fmt_error(&format!("Could not open PAK file: {}", err)).exit(),
}; };
let command = match cli.command { match cli.command {
Some(c) => c,
None => {
exit(0);
}
};
match command {
Commands::Extract { output } => { Commands::Extract { output } => {
if output.exists() && !output.is_dir() { if output.exists() && !output.is_dir() {
fmt_error("The output given was not a directory").exit() fmt_error("The output given was not a directory").exit()
@ -92,11 +74,7 @@ fn main() {
for entry in pak.entries() { for entry in pak.entries() {
let mut outpath = output.clone(); let mut outpath = output.clone();
if let Some(n) = entry.name() { outpath.push(entry.display_name());
outpath.push(n);
} else {
outpath.push(entry.index().to_string())
}
entry.save(&outpath).unwrap(); entry.save(&outpath).unwrap();
} }
} }

View file

@ -1,12 +0,0 @@
use owo_colors::OwoColorize;
pub fn version(bin_name: &str) -> String {
format!(
"{}, {} v{} ({}, {})",
bin_name,
env!("CARGO_PKG_NAME").cyan(),
env!("CARGO_PKG_VERSION").blue(),
(&env!("VERGEN_GIT_SHA")[0..=6]).green(),
env!("VERGEN_GIT_COMMIT_DATE").green(),
)
}