Compare commits

...

17 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
30 changed files with 1175 additions and 280 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

@ -3,7 +3,7 @@
</p>
# lbee-utils
# 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/).
@ -53,7 +53,7 @@ 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,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,4 +1,3 @@
use byteorder::{ReadBytesExt, WriteBytesExt, LE};
use std::{
collections::HashMap,
io::{Read, Seek, Write},
@ -6,6 +5,7 @@ use std::{
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> {
@ -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);

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,6 +1,6 @@
[package]
name = "utils"
version = "0.2.0"
name = "lbee-utils"
version = "0.2.1"
edition = "2021"
license = "GPL-3.0-or-later"
authors.workspace = true
@ -13,14 +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", features = ["derive", "error-context"] }
owo-colors = "4.1"
[build-dependencies]
vergen-git2 = { version = "1.0", features = ["build", "cargo", "rustc", "si"] }
vergen-gix = { version = "1.0", features = ["build", "cargo", "rustc", "si"] }
[lints]
workspace = true

View file

@ -1,17 +1,23 @@
use vergen_git2::{BuildBuilder, CargoBuilder, Emitter, Git2Builder, RustcBuilder, SysinfoBuilder};
use vergen_gix::{BuildBuilder, CargoBuilder, Emitter, GixBuilder, RustcBuilder, SysinfoBuilder};
fn main() {
let build = BuildBuilder::all_build().unwrap();
let cargo = CargoBuilder::all_cargo().unwrap();
let git2 = Git2Builder::all_git().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(&git2).unwrap()
.add_instructions(&rustc).unwrap()
.add_instructions(&si).unwrap()
.emit().unwrap();
.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,7 +1,12 @@
use clap::{error::ErrorKind, Command, 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::{
fs,
path::{Path, PathBuf}, process::exit,
path::{Path, PathBuf},
process::exit,
};
/// Utility to maniuplate CZ image files from the LUCA System game engine by
@ -21,7 +26,7 @@ struct Cli {
#[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)]
@ -36,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
@ -69,13 +93,7 @@ fn main() {
let cli = Cli::parse();
if cli.version {
println!(
"{}, {} v{}-{}",
env!("CARGO_BIN_NAME"),
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
&env!("VERGEN_GIT_SHA")[0..=6]
);
println!("{}", version(env!("CARGO_BIN_NAME")));
exit(0);
}
@ -92,28 +110,19 @@ fn main() {
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() {
@ -131,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();
}
}
}
@ -166,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
@ -236,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");
}
}
}
@ -268,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)?;
@ -292,3 +377,7 @@ fn replace_cz<P: ?Sized + AsRef<Path>>(
Ok(())
}
fn pretty_error(message: &str) {
eprintln!("{}: {}", "Error".red().italic(), message);
}

View file

@ -2,6 +2,7 @@ use clap::{
error::{Error, ErrorKind},
Parser, Subcommand,
};
use lbee_utils::version;
use luca_pak::Pak;
use std::{fs, path::PathBuf, process::exit};
@ -10,6 +11,7 @@ use std::{fs, path::PathBuf, process::exit};
#[derive(Parser)]
#[command(name = "PAK Utility")]
#[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)]
@ -64,13 +66,7 @@ fn main() {
let cli = Cli::parse();
if cli.version {
println!(
"{}, {} v{}-{}",
env!("CARGO_BIN_NAME"),
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
&env!("VERGEN_GIT_SHA")[0..=6]
);
println!("{}", version(env!("CARGO_BIN_NAME")));
exit(0);
}
@ -83,7 +79,7 @@ fn main() {
Some(c) => c,
None => {
exit(0);
},
}
};
match command {

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(),
)
}