Compare commits

...

12 commits

Author SHA1 Message Date
G2
7dfef2a2dc
Update README.md 2024-07-12 00:54:44 -05:00
78fceae000 Added CZ5 wanted message 2024-07-12 00:43:49 -05:00
G2
f75654fa18
Update README.md 2024-07-12 00:36:25 -05:00
5399a7dbb4 Updated README.md 2024-07-12 00:26:06 -05:00
4e0df5412d Changed versions and clap info output 2024-07-11 23:39:14 -05:00
e1a6a80659 Fixed version, remove clap from cz and luca_pak 2024-07-11 23:22:38 -05:00
df3f3b9373 Renamed experimental to pak_explorer, moved CLI utils to utils 2024-07-11 23:17:40 -05:00
17a29fef8e Updated versions, added versions to binaries 2024-07-11 22:59:06 -05:00
f88202221e Bumped version to 0.1.1 2024-07-11 22:54:17 -05:00
7b9140ae22 Fixed minor issues 2024-07-11 22:53:04 -05:00
3bd0980313 Bumped version 2024-07-11 22:39:36 -05:00
002eda0612 Fixed an edge case
If the offset size was equal to 2048, the program set it to 2048 instead of 0 incorrectly
2024-07-11 22:36:55 -05:00
13 changed files with 190 additions and 72 deletions

View file

@ -2,8 +2,8 @@
resolver = "2" resolver = "2"
members = [ members = [
"cz", "cz",
"experimental", "pak_explorer",
"luca_pak", "luca_pak", "utils",
] ]
[workspace.package] [workspace.package]

View file

@ -6,15 +6,18 @@ Tested on the following games:
- Little Busters! English Edition (2017) - Little Busters! English Edition (2017)
- LOOPERS (2023) - LOOPERS (2023)
- Harmonia Full HD Edition (2024) - Harmonia Full HD Edition (2024)
- Kanon (2024)
## Acknowledgments ## Acknowledgments
The implementation for decompression of CZ1, CZ3, and CZ4 was originally derived from The implementation for decompression of CZ1, CZ3, and CZ4 was originally
[GARbro](https://github.com/morkt/GARbro/). The implementation of compresssion derived from [GARbro](https://github.com/morkt/GARbro/). The implementation of
and decompression of CZ1, CZ2, CZ3, and CZ4 was derived from [LuckSystem](https://github.com/wetor/LuckSystem). compresssion and decompression of CZ1, CZ2, CZ3, and CZ4 was derived from
This project would not have been possible without their amazing work. [LuckSystem](https://github.com/wetor/LuckSystem). This project would not have
been possible without their amazing work.
## Features ## Features
These decoders and encoders are structured as libraries first and tools second. It's possible to use them as a base to build other applications. These decoders and encoders are structured as libraries first and tools second.
It's possible to use them as a base to build other applications.
### CZ Images ### CZ Images
Completely accurate CZ# file decoding and encoding. Read more about that here: Completely accurate CZ# file decoding and encoding. Read more about that here:
@ -22,4 +25,59 @@ Completely accurate CZ# file decoding and encoding. Read more about that here:
https://g2games.dev/blog/2024/06/28/the-cz-image-formats/ https://g2games.dev/blog/2024/06/28/the-cz-image-formats/
### PAK Archives ### PAK Archives
Partial implementation of PAK files, enough to extract data from most I've encountered, and replace data as long as decoding is successful. Any extra metadata can't be changed as of yet, however. Partial implementation of PAK files, enough to extract data from most I've
encountered, and replace data as long as decoding is successful. Any extra
metadata can't be changed as of yet, however.
## Programs
### [lbee-utils](https://github.com/G2-Games/lbee-utils/releases/tag/utils-0.1.0)
Small command line tools for modifying CZ images and PAK archives. Usage for each
is as follows:
#### pakutil
```
Utility to maniuplate PAK archive files from the LUCA System game engine by Prototype Ltd
Usage: pakutil <PAK FILE> <COMMAND>
Commands:
extract Extracts the contents of a PAK file into a folder
replace Replace the entries in a PAK file
help Print this message or the help of the given subcommand(s)
Arguments:
<PAK FILE>
Options:
-h, --help Print help
-V, --version Print version
```
#### czutil
```
Utility to maniuplate CZ image files from the LUCA System game engine by Prototype Ltd
Usage: czutil <COMMAND>
Commands:
decode Converts a CZ file to a PNG
replace Replace a CZ file's image data
help Print this message or the help of the given subcommand(s)
Options:
-h, --help Print help
-V, --version Print version
```
------
### [PAK Explorer](https://github.com/G2-Games/lbee-utils/releases/tag/explorer-0.1.1)
This is a basic explorer application for PAK files which allows you to see
their contents, replace the contents, extract files, and save them again.
While this is a useful tool for just viewing and extracting the contents of
a PAK file, it is recommended to use the command line tools for doing
anything important as they offer many more options and allow for batch
operations on many files at once.
![image](https://github.com/user-attachments/assets/0ae93c40-a951-45a7-b5ee-17b60aa96157)

View file

@ -1,20 +1,16 @@
[package] [package]
name = "cz" name = "cz"
edition = "2021" edition = "2021"
version = "0.1.0" version = "0.1.2"
description=""" description="""
A encoder/decoder for CZ# image files used in the LUCA System Engine. An encoder/decoder for CZ# image files used in the LUCA System engine by
Prototype Ltd.
""" """
license = "MIT" license = "MIT"
authors.workspace = true authors.workspace = true
[features] [features]
png = ["dep:image"] png = ["dep:image"]
binary = ["dep:clap"]
[[bin]]
name = "czutil"
required-features = ["binary", "png"]
[dependencies] [dependencies]
byteorder = "1.5" byteorder = "1.5"
@ -22,11 +18,8 @@ thiserror = "1.0"
imagequant = "4.3" imagequant = "4.3"
rgb = "0.8" rgb = "0.8"
# Only active on features "png" and "binary" # Only active on PNG feature
image = { version = "0.25", optional = true } image = { version = "0.25", optional = true }
# Only active on feature "binary"
clap = { version = "4.5.8", features = ["derive"], optional = true }
[lints] [lints]
workspace = true workspace = true

View file

@ -54,7 +54,7 @@ impl DynamicCz {
CzVersion::CZ2 => cz2::decode(input)?, CzVersion::CZ2 => cz2::decode(input)?,
CzVersion::CZ3 => cz3::decode(input, &header_common)?, CzVersion::CZ3 => cz3::decode(input, &header_common)?,
CzVersion::CZ4 => cz4::decode(input, &header_common)?, CzVersion::CZ4 => cz4::decode(input, &header_common)?,
CzVersion::CZ5 => unimplemented!(), CzVersion::CZ5 => unimplemented!("CZ5 files are not implemented! Please contact the application developers about this file."),
}; };
let image_size = header_common.width() as usize * header_common.height() as usize; let image_size = header_common.width() as usize * header_common.height() as usize;

View file

@ -1,21 +1,16 @@
[package] [package]
name = "luca_pak" name = "luca_pak"
edition = "2021" edition = "2021"
version = "0.1.1" version = "0.1.2"
description = """ description = """
A crate for parsing and modifying PAK files from the LUCA System engine by An encoder/decoder for PAK archive files used in the LUCA System engine by
Prototype Ltd. Prototype Ltd.
""" """
license = "MIT" license = "MIT"
authors.workspace = true authors.workspace = true
[[bin]]
name = "pakutil"
path = "src/main.rs"
[dependencies] [dependencies]
byteorder = "1.5.0" byteorder = "1.5.0"
clap = { version = "4.5.8", features = ["derive"] }
log = "0.4.22" log = "0.4.22"
thiserror = "1.0.61" thiserror = "1.0.61"

View file

@ -1,5 +1,5 @@
use std::{ use std::{
error::Error, fmt, fs::File, io::{BufWriter, Write}, path::Path error::Error, fs::File, io::{BufWriter, Write}, path::Path
}; };
/// A single file entry in a PAK file /// A single file entry in a PAK file
@ -33,6 +33,10 @@ impl Entry {
&self.name &self.name
} }
pub fn index(&self) -> usize {
self.index
}
pub fn id(&self) -> u32 { pub fn id(&self) -> u32 {
self.id self.id
} }

View file

@ -14,7 +14,8 @@ pub struct Header {
pub(super) id_start: u32, pub(super) id_start: u32,
pub(super) block_size: u32, pub(super) block_size: u32,
pub(super) unknown1: u32, /// The offset of the subdirectory name within the PAK
pub(super) subdir_offset: u32,
pub(super) unknown2: u32, pub(super) unknown2: u32,
pub(super) unknown3: u32, pub(super) unknown3: u32,
pub(super) unknown4: u32, pub(super) unknown4: u32,
@ -28,7 +29,7 @@ impl Header {
output.write_u32::<LE>(self.entry_count)?; output.write_u32::<LE>(self.entry_count)?;
output.write_u32::<LE>(self.id_start)?; output.write_u32::<LE>(self.id_start)?;
output.write_u32::<LE>(self.block_size)?; output.write_u32::<LE>(self.block_size)?;
output.write_u32::<LE>(self.unknown1)?; output.write_u32::<LE>(self.subdir_offset)?;
output.write_u32::<LE>(self.unknown2)?; output.write_u32::<LE>(self.unknown2)?;
output.write_u32::<LE>(self.unknown3)?; output.write_u32::<LE>(self.unknown3)?;
output.write_u32::<LE>(self.unknown4)?; output.write_u32::<LE>(self.unknown4)?;

View file

@ -33,6 +33,8 @@ pub enum PakError {
/// A full PAK file with a header and its contents /// A full PAK file with a header and its contents
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Pak { pub struct Pak {
subdirectory: Option<String>,
/// The path of the PAK file, can serve as an identifier or name as the /// The path of the PAK file, can serve as an identifier or name as the
/// header has no name for the file. /// header has no name for the file.
path: PathBuf, path: PathBuf,
@ -72,7 +74,7 @@ impl Pak {
entry_count: input.read_u32::<LE>()?, entry_count: input.read_u32::<LE>()?,
id_start: input.read_u32::<LE>()?, id_start: input.read_u32::<LE>()?,
block_size: input.read_u32::<LE>()?, block_size: input.read_u32::<LE>()?,
unknown1: input.read_u32::<LE>()?, subdir_offset: input.read_u32::<LE>()?,
unknown2: input.read_u32::<LE>()?, unknown2: input.read_u32::<LE>()?,
unknown3: input.read_u32::<LE>()?, unknown3: input.read_u32::<LE>()?,
unknown4: input.read_u32::<LE>()?, unknown4: input.read_u32::<LE>()?,
@ -130,16 +132,15 @@ impl Pak {
// Read all the file names // Read all the file names
let mut file_names = None; let mut file_names = None;
let mut subdirectory = None;
if header.flags.has_names() { if header.flags.has_names() {
debug!("READING: file_names"); debug!("READING: file_names");
let mut string_buf = Vec::new(); if header.subdir_offset != 0 {
subdirectory = Some(read_cstring(&mut input)?);
}
file_names = Some(Vec::new()); file_names = Some(Vec::new());
for _ in 0..header.entry_count() { for _ in 0..header.entry_count() {
string_buf.clear(); let strbuf = read_cstring(&mut input)?;
input.read_until(0x00, &mut string_buf)?;
string_buf.pop();
let strbuf = String::from_utf8_lossy(&string_buf).to_string();
file_names.as_mut().unwrap().push(strbuf.clone()); file_names.as_mut().unwrap().push(strbuf.clone());
} }
} }
@ -189,6 +190,7 @@ impl Pak {
debug!("Entry list contains {} entries", entries.len()); debug!("Entry list contains {} entries", entries.len());
Ok(Pak { Ok(Pak {
subdirectory,
header, header,
unknown_pre_data, unknown_pre_data,
entries, entries,
@ -211,7 +213,6 @@ impl Pak {
&self, &self,
mut output: &mut T mut output: &mut T
) -> Result<(), PakError> { ) -> Result<(), PakError> {
let mut block_offset = 0;
self.header.write_into(&mut output)?; self.header.write_into(&mut output)?;
// Write unknown data // Write unknown data
@ -237,6 +238,11 @@ impl Pak {
// Write names if the flags indicate it should have them // Write names if the flags indicate it should have them
if self.header.flags().has_names() { if self.header.flags().has_names() {
if let Some(subdir) = &self.subdirectory {
output.write_all(
CString::new(subdir.as_bytes()).unwrap().to_bytes_with_nul()
)?;
}
for entry in self.entries() { for entry in self.entries() {
let name = entry.name.as_ref().unwrap(); let name = entry.name.as_ref().unwrap();
output.write_all( output.write_all(
@ -247,18 +253,22 @@ impl Pak {
output.write_all(&self.unknown_post_header)?; output.write_all(&self.unknown_post_header)?;
block_offset += self.header().data_offset / self.header().block_size; //let mut block_offset = self.header().data_offset / self.header().block_size;
for entry in self.entries() { for entry in self.entries() {
let block_size = entry.data.len().div_ceil(self.header().block_size as usize); //let block_size = entry.data.len().div_ceil(self.header().block_size as usize);
let remainder = 2048 - entry.data.len().rem_euclid(self.header().block_size as usize); let mut remainder = 2048 - entry.data.len().rem_euclid(self.header().block_size as usize);
if remainder == 2048 {
debug!("entry {:?} len {}", entry.name(), entry.data.len()); remainder = 0;
debug!("remainder {}", remainder); }
debug!("block_offset {} - expected offset {}", block_offset, entry.offset);
output.write_all(&entry.data)?; output.write_all(&entry.data)?;
output.write_all(&vec![0u8; remainder])?; output.write_all(&vec![0u8; remainder])?;
block_offset += block_size as u32;
//println!("entry len {}", entry.data.len());
//println!("remainder {}", remainder);
//println!("block_offset {} - expected offset {}", block_offset, entry.offset);
//block_offset += block_size as u32;
} }
Ok(()) Ok(())
@ -384,3 +394,11 @@ impl Pak {
.is_some_and(|n| n == name)) .is_some_and(|n| n == name))
} }
} }
fn read_cstring<T: Seek + Read + BufRead>(input: &mut T) -> Result<String, io::Error> {
let mut string_buf = vec![];
input.read_until(0x00, &mut string_buf)?;
string_buf.pop();
Ok(String::from_utf8_lossy(&string_buf).to_string())
}

View file

@ -1,10 +1,11 @@
[package] [package]
name = "experimental" name = "pak_explorer"
description = """
A package for experimentation, not for publishing
"""
version = "0.1.0"
edition = "2021" edition = "2021"
version = "0.1.1"
description = """
A simple GUI for exploring and making modifications to LUCA System PAK files.
"""
license = "AGPL-3.0"
authors.workspace = true authors.workspace = true
publish = false publish = false

View file

@ -1,9 +1,9 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] // hide console window on Windows in release
use std::path::PathBuf; use std::fs;
use eframe::{egui::{self, text::{LayoutJob, TextWrapping}, ColorImage, Image, Rgba, TextBuffer, TextureFilter, TextureHandle, TextureOptions}, epaint::Fonts}; use eframe::egui::{self, ColorImage, Image, TextureFilter, TextureHandle, TextureOptions};
use luca_pak::{entry::EntryType, header, Pak}; use luca_pak::{entry::EntryType, Pak};
fn main() -> eframe::Result { fn main() -> eframe::Result {
let options = eframe::NativeOptions { let options = eframe::NativeOptions {
@ -47,6 +47,7 @@ impl eframe::App for PakExplorer {
ctx.set_pixels_per_point(1.5); ctx.set_pixels_per_point(1.5);
ui.heading("PAK File Explorer"); ui.heading("PAK File Explorer");
ui.horizontal(|ui| {
if ui.button("Open file…").clicked() { if ui.button("Open file…").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_file() { if let Some(path) = rfd::FileDialog::new().pick_file() {
let pak = Pak::open(&path).unwrap(); let pak = Pak::open(&path).unwrap();
@ -56,6 +57,17 @@ impl eframe::App for PakExplorer {
self.hex_string = None; self.hex_string = None;
} }
} }
if let Some(pak) = &self.open_file {
if ui.button("Save PAK…").clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_file_name(pak.path().file_name().unwrap().to_string_lossy())
.save_file()
{
pak.save(&path).unwrap();
}
}
}
});
ui.separator(); ui.separator();
@ -92,6 +104,7 @@ impl eframe::App for PakExplorer {
} }
if let Some(entry) = &self.selected_entry { if let Some(entry) = &self.selected_entry {
ui.horizontal(|ui| {
if ui.button("Save entry…").clicked() { if ui.button("Save entry…").clicked() {
if let Some(path) = rfd::FileDialog::new() if let Some(path) = rfd::FileDialog::new()
.set_file_name(entry.display_name()) .set_file_name(entry.display_name())
@ -100,6 +113,16 @@ impl eframe::App for PakExplorer {
entry.save(&path).unwrap(); entry.save(&path).unwrap();
} }
} }
if let Some(pak) = &mut self.open_file.as_mut() {
if ui.button("Replace entry…").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_file() {
let file_bytes = fs::read(path).unwrap();
pak.replace(entry.index(), &file_bytes).unwrap();
}
}
}
});
match entry.file_type() { match entry.file_type() {
EntryType::CZ0 | EntryType::CZ1 EntryType::CZ0 | EntryType::CZ1
| EntryType::CZ2 | EntryType::CZ3 | EntryType::CZ2 | EntryType::CZ3
@ -147,7 +170,7 @@ impl eframe::App for PakExplorer {
); );
}, },
} }
} else { } else if self.open_file.is_some() {
ui.centered_and_justified(|ui| ui.centered_and_justified(|ui|
ui.label("Select an Entry") ui.label("Select an Entry")
); );

21
utils/Cargo.toml Normal file
View file

@ -0,0 +1,21 @@
[package]
name = "utils"
version = "0.1.0"
edition = "2021"
authors.workspace = true
[[bin]]
name = "czutil"
[[bin]]
name = "pakutil"
[dependencies]
cz = { path = "../cz/", features = ["png"] }
luca_pak = { path = "../luca_pak/" }
image = { version = "0.25", default-features = false, features = ["png"] }
clap = { version = "4.5.9", features = ["derive"] }
[lints]
workspace = true

View file

@ -1,8 +1,10 @@
use clap::{error::ErrorKind, Error, Parser, Subcommand}; use clap::{error::ErrorKind, Error, Parser, Subcommand};
use std::{fs, path::{Path, PathBuf}}; use std::{fs, path::{Path, PathBuf}};
/// Utility to maniuplate CZ image files from the LUCA System game engine by
/// Prototype Ltd.
#[derive(Parser)] #[derive(Parser)]
#[command(name = "CZ Utils")] #[command(name = "CZ Utility")]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
struct Cli { struct Cli {
#[command(subcommand)] #[command(subcommand)]

View file

@ -2,8 +2,10 @@ use std::{fs, path::PathBuf};
use clap::{error::{Error, ErrorKind}, Parser, Subcommand}; use clap::{error::{Error, ErrorKind}, Parser, Subcommand};
use luca_pak::Pak; use luca_pak::Pak;
/// Utility to maniuplate PAK archive files from the LUCA System game engine by
/// Prototype Ltd.
#[derive(Parser)] #[derive(Parser)]
#[command(name = "CZ Utils")] #[command(name = "PAK Utility")]
#[command(version, about, long_about = None)] #[command(version, about, long_about = None)]
struct Cli { struct Cli {
#[arg(value_name = "PAK FILE")] #[arg(value_name = "PAK FILE")]