Compare commits

...

24 commits

Author SHA1 Message Date
3e4b51cad5 Fixed libasound2 installation 2025-03-10 12:21:11 -05:00
250ff2255c Updated workflow to fix pak explorer build 2025-03-10 12:18:41 -05:00
d33323fd87 Updated version, changed to Rust 2024 2025-03-10 12:18:20 -05:00
982f55fde6 Added rather unfortunate audio playback to pak explorer 2025-03-08 23:36:31 -06:00
1ffc88d379 Improved reader, started work on writer 2024-11-21 13:29:33 -06:00
957f3e637d More script parsing improvements 2024-11-21 10:50:51 -06:00
3437b6c7a9 Added preliminary script parsing 2024-11-16 04:29:43 -06:00
3c4d7a89ec Updated readme 2024-11-15 21:51:43 -06:00
01fdb340fa Applied clippy suggestions, ran cargo fmt 2024-11-14 13:04:15 -06:00
b74dc0344e Improved encode command 2024-11-14 12:59:34 -06:00
e68ca53ab5 Renamed DynamicCz to CzFile, added Encode option for czutil 2024-11-14 12:43:27 -06:00
a7a486999d Upgraded dependencies, reduced binary sizes 2024-11-14 02:11:39 -06:00
2f881ce294 Merge branch 'main' of https://github.com/G2-Games/lbee-utils 2024-11-13 10:53:17 -06:00
dd99a0d834 Removed image as a dependency from cz 2024-11-13 10:53:15 -06:00
b36ea64d84 Changed to gix from git2 in version message creation 2024-11-13 02:38:50 -06:00
ec60308e6a Replaced byteorder with byteorder-lite, ran cargo fmt 2024-11-12 19:50:10 -06:00
G2
d253d57816
Update README.md 2024-11-12 03:17:27 -06:00
263a990e6e Fixed many issues with utils, v0.2.0 2024-10-18 16:13:29 -05:00
db52e23ef7 Revert changes to binio.rs 2024-10-18 14:21:24 -05:00
G2
4d58df5d15
Bumped version - v0.1.2 2024-10-04 12:36:22 -04:00
G2
be0aaeeeb6
Update README.md, removed accidental images 2024-09-29 01:25:26 -05:00
G2
aaf7f9b30f
Update README.md, added logo 2024-09-29 01:24:32 -05:00
G2
a2c62ffc4a
Fix typo in README.md 2024-09-24 14:33:34 -05:00
G2
de6f543898
Update README.md 2024-09-24 14:33:11 -05:00
31 changed files with 1307 additions and 415 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
use byteorder::ReadBytesExt;
use byteorder_lite::ReadBytesExt;
use rgb::ComponentSlice;
use std::{
fs::File,
@ -13,7 +13,7 @@ use crate::{
/// A CZ# interface which can open and save any CZ file type.
#[derive(Debug, Clone)]
pub struct DynamicCz {
pub struct CzFile {
header_common: CommonHeader,
header_extended: Option<ExtendedHeader>,
@ -24,7 +24,7 @@ pub struct DynamicCz {
bitmap: Vec<u8>,
}
impl DynamicCz {
impl CzFile {
/// Decode a CZ# file from anything that implements [`Read`] and [`Seek`]
///
/// The input must begin with the
@ -192,35 +192,6 @@ impl DynamicCz {
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
/// is the only format that is used internally.
pub fn from_raw(version: CzVersion, width: u16, height: u16, bitmap: Vec<u8>) -> Self {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -58,15 +58,30 @@ impl Entry {
self.length as usize
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Get the raw byte data of an [`Entry`]
pub fn as_bytes(&self) -> &Vec<u8> {
&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 {
let mut name = self.name().clone().unwrap_or(self.id().to_string());
let entry_type = self.file_type();
name.push_str(&entry_type.extension());
name.push_str(entry_type.extension());
name
}
@ -84,6 +99,12 @@ impl Entry {
}
} else if self.data[0..3] == [b'M', b'V', b'T'] {
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 {
EntryType::Unknown
}
@ -92,6 +113,7 @@ impl Entry {
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EntryType {
// CZ image files
CZ0,
CZ1,
CZ2,
@ -102,6 +124,14 @@ pub enum EntryType {
/// An MVT video file
MVT,
/// OGG Audio file
OGG,
/// OGGPAK Audio file
OGGPAK,
/// Wav Audio file
WAV,
/// Who knows!
Unknown,
}
@ -117,6 +147,9 @@ impl EntryType {
Self::CZ4 => ".cz4",
Self::CZ5 => ".cz5",
Self::MVT => ".mvt",
Self::OGG => ".ogg",
Self::OGGPAK => ".oggpak",
Self::WAV => ".wav",
Self::Unknown => "",
}
}

View file

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

View file

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

14
luca_script/Cargo.toml Normal file
View file

@ -0,0 +1,14 @@
[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

@ -0,0 +1,107 @@
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

582
luca_script/src/main.rs Normal file
View file

@ -0,0 +1,582 @@
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,
}
}
}

70
luca_script/src/utils.rs Normal file
View file

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

View file

@ -1,10 +1,12 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use colog;
use eframe::egui::{self, ColorImage, Image, TextureFilter, TextureHandle, TextureOptions};
use eframe::egui::{
self, ColorImage, Image, ProgressBar, TextureFilter, TextureHandle, TextureOptions, ThemePreference
};
use kira::{sound::static_sound::{StaticSoundData, StaticSoundHandle}, AudioManager, AudioManagerSettings, DefaultBackend, Tween};
use log::error;
use luca_pak::{entry::EntryType, Pak};
use std::fs;
use std::{fs, io::Cursor, time::Duration};
fn main() -> eframe::Result {
colog::default_builder()
@ -13,17 +15,26 @@ fn main() -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default().with_inner_size([1024.0, 800.0]),
follow_system_theme: true,
..Default::default()
};
let manager = AudioManager::<DefaultBackend>::new(AudioManagerSettings::default()).unwrap();
eframe::run_native(
"LUCA PAK Explorer",
options,
Box::new(|ctx| {
let ppp = ctx.egui_ctx.pixels_per_point() * 1.5;
ctx.egui_ctx.set_pixels_per_point(ppp);
Ok(Box::<PakExplorer>::default())
Ok(Box::new(PakExplorer {
open_file: None,
selected_entry: None,
image_texture: None,
hex_string: None,
audio_player: manager,
audio_handle: None,
audio_duration: None,
}))
}),
)
}
@ -31,27 +42,18 @@ fn main() -> eframe::Result {
struct PakExplorer {
open_file: Option<Pak>,
selected_entry: Option<luca_pak::entry::Entry>,
entry_text: String,
image_texture: Option<egui::TextureHandle>,
hex_string: Option<Vec<String>>,
}
impl Default for PakExplorer {
fn default() -> Self {
Self {
open_file: None,
selected_entry: None,
image_texture: None,
hex_string: None,
entry_text: String::new(),
}
}
audio_player: AudioManager,
audio_handle: Option<StaticSoundHandle>,
audio_duration: Option<Duration>,
}
impl eframe::App for PakExplorer {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("PAK File Explorer");
ctx.options_mut(|o| o.theme_preference = ThemePreference::System);
ui.horizontal(|ui| {
if ui.button("Open file").clicked() {
@ -67,6 +69,13 @@ impl eframe::App for PakExplorer {
self.selected_entry = None;
self.image_texture = 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 {
@ -97,7 +106,7 @@ impl eframe::App for PakExplorer {
};
ui.horizontal(|ui| {
egui::ComboBox::from_id_source("my-combobox")
egui::ComboBox::from_id_salt("my-combobox")
.selected_text(selection.clone())
.truncate()
.show_ui(ui, |ui| {
@ -112,6 +121,13 @@ impl eframe::App for PakExplorer {
.clicked()
{
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;
};
}
});
@ -158,11 +174,18 @@ impl eframe::App for PakExplorer {
.set_file_name(display_name)
.save_file()
{
let cz = cz::DynamicCz::decode(&mut std::io::Cursor::new(
entry.as_bytes(),
))
let cz =
cz::CzFile::decode(&mut std::io::Cursor::new(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();
cz.save_as_png(&path).unwrap();
}
}
@ -170,7 +193,7 @@ impl eframe::App for PakExplorer {
let texture: &TextureHandle = self.image_texture.get_or_insert_with(|| {
let cz =
cz::DynamicCz::decode(&mut std::io::Cursor::new(entry.as_bytes()))
cz::CzFile::decode(&mut std::io::Cursor::new(entry.as_bytes()))
.unwrap();
let image = ColorImage::from_rgba_unmultiplied(
[cz.header().width() as usize, cz.header().height() as usize],
@ -196,6 +219,46 @@ 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"));
}

View file

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

23
utils/build.rs Normal file
View file

@ -0,0 +1,23 @@
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,22 +1,32 @@
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::{
fs,
path::{Path, PathBuf},
process::exit,
};
/// Utility to maniuplate CZ image files from the LUCA System game engine by
/// Prototype Ltd.
#[derive(Parser)]
#[command(name = "CZ Utility")]
#[command(version, about, long_about = None)]
#[command(author, version, about, long_about = None, disable_version_flag = true)]
#[command(arg_required_else_help(true))]
struct Cli {
/// Show program version information
#[arg(short('V'), long)]
version: bool,
#[command(subcommand)]
command: Commands,
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
/// Converts a CZ file to a PNG
/// Decode a CZ file to a PNG
Decode {
/// Decode a whole folder, and output to another folder
#[arg(short, long)]
@ -31,7 +41,26 @@ enum Commands {
output: Option<PathBuf>,
},
/// Replace a CZ file's image data
/// Encode a PNG file to a CZ
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 a whole folder, and output to another folder,
/// using a folder of replacements
@ -63,36 +92,37 @@ enum Commands {
fn main() {
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
match &cli.command {
match &command {
Commands::Decode {
input,
output,
batch,
} => {
if !input.exists() {
Error::raw(
ErrorKind::ValueValidation,
"The input file/folder provided does not exist\n",
)
.exit()
pretty_error("The input file/folder provided does not exist");
exit(1);
}
if *batch {
if input.is_file() {
Error::raw(
ErrorKind::ValueValidation,
"Batch input must be a directory\n",
)
.exit()
pretty_error("Batch input must be a directory");
exit(1);
}
if output.is_none() || output.as_ref().unwrap().is_file() {
Error::raw(
ErrorKind::ValueValidation,
"Batch output must be a directory\n",
)
.exit()
pretty_error("Batch output must be a directory");
exit(1);
}
for entry in fs::read_dir(input).unwrap() {
@ -110,29 +140,48 @@ fn main() {
let cz = match cz::open(&path) {
Ok(cz) => cz,
Err(_) => {
Error::raw(
ErrorKind::ValueValidation,
format!(
"Could not open input as a CZ file: {}\n",
path.into_os_string().to_str().unwrap()
),
)
.print()
.unwrap();
pretty_error(&format!(
"Could not open input as a CZ file: {}\n",
path.into_os_string().to_str().unwrap()
));
continue;
}
};
cz.save_as_png(&final_path).unwrap();
image::save_buffer_with_format(
final_path,
cz.as_raw(),
cz.header().width() as u32,
cz.header().height() as u32,
ColorType::Rgba8,
image::ImageFormat::Png,
)
.unwrap();
}
} else {
let cz = cz::open(input).unwrap();
if let Some(output) = output {
cz.save_as_png(output).unwrap();
image::save_buffer_with_format(
output,
cz.as_raw(),
cz.header().width() as u32,
cz.header().height() as u32,
ColorType::Rgba8,
image::ImageFormat::Png,
)
.unwrap();
} else {
let file_stem = PathBuf::from(input.file_name().unwrap());
cz.save_as_png(&file_stem.with_extension("png")).unwrap();
image::save_buffer_with_format(
file_stem.with_extension("png"),
cz.as_raw(),
cz.header().width() as u32,
cz.header().height() as u32,
ColorType::Rgba8,
image::ImageFormat::Png,
)
.unwrap();
}
}
}
@ -145,45 +194,30 @@ fn main() {
depth,
} => {
if !input.exists() {
Error::raw(
ErrorKind::ValueValidation,
"The original file provided does not exist\n",
)
.exit()
pretty_error("The input file does not exist");
exit(1);
}
if !replacement.exists() {
Error::raw(
ErrorKind::ValueValidation,
"The replacement file provided does not exist\n",
)
.exit()
pretty_error("The replacement file does not exist");
exit(1);
}
// If it's a batch replacement, we want directories to search
if *batch {
if !input.is_dir() {
Error::raw(
ErrorKind::ValueValidation,
"Batch input location must be a directory\n",
)
.exit()
pretty_error("Batch input must be a directory");
exit(1);
}
if !replacement.is_dir() {
Error::raw(
ErrorKind::ValueValidation,
"Batch replacement location must be a directory\n",
)
.exit()
pretty_error("Batch replacement must be a directory");
exit(1);
}
if !output.is_dir() {
Error::raw(
ErrorKind::ValueValidation,
"Batch output location must be a directory\n",
)
.exit()
pretty_error("Batch output location must be a directory");
exit(1);
}
// Replace all the files within the directory and print errors for them
@ -215,17 +249,89 @@ fn main() {
}
} else {
if !input.is_file() {
Error::raw(ErrorKind::ValueValidation, "Input must be a file\n").exit()
pretty_error("Input must be a file");
exit(1);
}
if !replacement.is_file() {
Error::raw(ErrorKind::ValueValidation, "Replacement must be a file\n").exit()
pretty_error("Replacement must be a file");
exit(1);
}
// Replace the input file with the new image
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");
}
}
}
@ -247,7 +353,7 @@ fn replace_cz<P: ?Sized + AsRef<Path>>(
}
// 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
let mut cz = cz::open(&path)?;
@ -271,3 +377,7 @@ fn replace_cz<P: ?Sized + AsRef<Path>>(
Ok(())
}
fn pretty_error(message: &str) {
eprintln!("{}: {}", "Error".red().italic(), message);
}

View file

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

12
utils/src/lib.rs Normal file
View file

@ -0,0 +1,12 @@
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(),
)
}