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"
members = [
"cz",
"experimental",
"luca_pak",
"pak_explorer",
"luca_pak", "utils",
]
[workspace.package]

View file

@ -6,15 +6,18 @@ Tested on the following games:
- Little Busters! English Edition (2017)
- LOOPERS (2023)
- Harmonia Full HD Edition (2024)
- Kanon (2024)
## Acknowledgments
The implementation for decompression of CZ1, CZ3, and CZ4 was originally derived from
[GARbro](https://github.com/morkt/GARbro/). The implementation of compresssion
and decompression of CZ1, CZ2, CZ3, and CZ4 was derived from [LuckSystem](https://github.com/wetor/LuckSystem).
This project would not have been possible without their amazing work.
The implementation for decompression of CZ1, CZ3, and CZ4 was originally
derived from [GARbro](https://github.com/morkt/GARbro/). The implementation of
compresssion and decompression of CZ1, CZ2, CZ3, and CZ4 was derived from
[LuckSystem](https://github.com/wetor/LuckSystem). This project would not have
been possible without their amazing work.
## 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
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/
### 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]
name = "cz"
edition = "2021"
version = "0.1.0"
version = "0.1.2"
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"
authors.workspace = true
[features]
png = ["dep:image"]
binary = ["dep:clap"]
[[bin]]
name = "czutil"
required-features = ["binary", "png"]
[dependencies]
byteorder = "1.5"
@ -22,11 +18,8 @@ thiserror = "1.0"
imagequant = "4.3"
rgb = "0.8"
# Only active on features "png" and "binary"
# Only active on PNG feature
image = { version = "0.25", optional = true }
# Only active on feature "binary"
clap = { version = "4.5.8", features = ["derive"], optional = true }
[lints]
workspace = true

View file

@ -54,7 +54,7 @@ impl DynamicCz {
CzVersion::CZ2 => cz2::decode(input)?,
CzVersion::CZ3 => cz3::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;

View file

@ -1,21 +1,16 @@
[package]
name = "luca_pak"
edition = "2021"
version = "0.1.1"
version = "0.1.2"
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.
"""
license = "MIT"
authors.workspace = true
[[bin]]
name = "pakutil"
path = "src/main.rs"
[dependencies]
byteorder = "1.5.0"
clap = { version = "4.5.8", features = ["derive"] }
log = "0.4.22"
thiserror = "1.0.61"

View file

@ -1,5 +1,5 @@
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
@ -33,6 +33,10 @@ impl Entry {
&self.name
}
pub fn index(&self) -> usize {
self.index
}
pub fn id(&self) -> u32 {
self.id
}

View file

@ -14,7 +14,8 @@ pub struct Header {
pub(super) id_start: 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) unknown3: u32,
pub(super) unknown4: u32,
@ -28,7 +29,7 @@ impl Header {
output.write_u32::<LE>(self.entry_count)?;
output.write_u32::<LE>(self.id_start)?;
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.unknown3)?;
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
#[derive(Debug, Clone)]
pub struct Pak {
subdirectory: Option<String>,
/// The path of the PAK file, can serve as an identifier or name as the
/// header has no name for the file.
path: PathBuf,
@ -72,7 +74,7 @@ impl Pak {
entry_count: input.read_u32::<LE>()?,
id_start: 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>()?,
unknown3: input.read_u32::<LE>()?,
unknown4: input.read_u32::<LE>()?,
@ -130,16 +132,15 @@ impl Pak {
// Read all the file names
let mut file_names = None;
let mut subdirectory = None;
if header.flags.has_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());
for _ in 0..header.entry_count() {
string_buf.clear();
input.read_until(0x00, &mut string_buf)?;
string_buf.pop();
let strbuf = String::from_utf8_lossy(&string_buf).to_string();
let strbuf = read_cstring(&mut input)?;
file_names.as_mut().unwrap().push(strbuf.clone());
}
}
@ -189,6 +190,7 @@ impl Pak {
debug!("Entry list contains {} entries", entries.len());
Ok(Pak {
subdirectory,
header,
unknown_pre_data,
entries,
@ -211,7 +213,6 @@ impl Pak {
&self,
mut output: &mut T
) -> Result<(), PakError> {
let mut block_offset = 0;
self.header.write_into(&mut output)?;
// Write unknown data
@ -237,6 +238,11 @@ impl Pak {
// Write names if the flags indicate it should have them
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() {
let name = entry.name.as_ref().unwrap();
output.write_all(
@ -247,18 +253,22 @@ impl Pak {
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() {
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);
debug!("entry {:?} len {}", entry.name(), entry.data.len());
debug!("remainder {}", remainder);
debug!("block_offset {} - expected offset {}", block_offset, entry.offset);
//let block_size = entry.data.len().div_ceil(self.header().block_size as usize);
let mut remainder = 2048 - entry.data.len().rem_euclid(self.header().block_size as usize);
if remainder == 2048 {
remainder = 0;
}
output.write_all(&entry.data)?;
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(())
@ -384,3 +394,11 @@ impl Pak {
.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]
name = "experimental"
description = """
A package for experimentation, not for publishing
"""
version = "0.1.0"
name = "pak_explorer"
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
publish = false

View file

@ -1,9 +1,9 @@
#![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 luca_pak::{entry::EntryType, header, Pak};
use eframe::egui::{self, ColorImage, Image, TextureFilter, TextureHandle, TextureOptions};
use luca_pak::{entry::EntryType, Pak};
fn main() -> eframe::Result {
let options = eframe::NativeOptions {
@ -47,6 +47,7 @@ impl eframe::App for PakExplorer {
ctx.set_pixels_per_point(1.5);
ui.heading("PAK File Explorer");
ui.horizontal(|ui| {
if ui.button("Open file…").clicked() {
if let Some(path) = rfd::FileDialog::new().pick_file() {
let pak = Pak::open(&path).unwrap();
@ -56,6 +57,17 @@ impl eframe::App for PakExplorer {
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();
@ -92,6 +104,7 @@ impl eframe::App for PakExplorer {
}
if let Some(entry) = &self.selected_entry {
ui.horizontal(|ui| {
if ui.button("Save entry…").clicked() {
if let Some(path) = rfd::FileDialog::new()
.set_file_name(entry.display_name())
@ -100,6 +113,16 @@ impl eframe::App for PakExplorer {
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() {
EntryType::CZ0 | EntryType::CZ1
| 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.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 std::{fs, path::{Path, PathBuf}};
/// Utility to maniuplate CZ image files from the LUCA System game engine by
/// Prototype Ltd.
#[derive(Parser)]
#[command(name = "CZ Utils")]
#[command(name = "CZ Utility")]
#[command(version, about, long_about = None)]
struct Cli {
#[command(subcommand)]

View file

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