mirror of
https://github.com/G2-Games/lbee-utils.git
synced 2025-06-22 22:52:55 -05:00
Compare commits
12 commits
f6374eecfc
...
7dfef2a2dc
Author | SHA1 | Date | |
---|---|---|---|
7dfef2a2dc | |||
78fceae000 | |||
f75654fa18 | |||
5399a7dbb4 | |||
4e0df5412d | |||
e1a6a80659 | |||
df3f3b9373 | |||
17a29fef8e | |||
f88202221e | |||
7b9140ae22 | |||
3bd0980313 | |||
002eda0612 |
13 changed files with 190 additions and 72 deletions
|
@ -2,8 +2,8 @@
|
|||
resolver = "2"
|
||||
members = [
|
||||
"cz",
|
||||
"experimental",
|
||||
"luca_pak",
|
||||
"pak_explorer",
|
||||
"luca_pak", "utils",
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
|
|
70
README.md
70
README.md
|
@ -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.
|
||||
|
||||

|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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)?;
|
||||
|
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
@ -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,15 +47,27 @@ impl eframe::App for PakExplorer {
|
|||
ctx.set_pixels_per_point(1.5);
|
||||
ui.heading("PAK File Explorer");
|
||||
|
||||
if ui.button("Open file…").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new().pick_file() {
|
||||
let pak = Pak::open(&path).unwrap();
|
||||
self.open_file = Some(pak);
|
||||
self.selected_entry = None;
|
||||
self.image_texture = None;
|
||||
self.hex_string = None;
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Open file…").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new().pick_file() {
|
||||
let pak = Pak::open(&path).unwrap();
|
||||
self.open_file = Some(pak);
|
||||
self.selected_entry = None;
|
||||
self.image_texture = 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();
|
||||
|
||||
|
@ -92,14 +104,25 @@ impl eframe::App for PakExplorer {
|
|||
}
|
||||
|
||||
if let Some(entry) = &self.selected_entry {
|
||||
if ui.button("Save entry…").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new()
|
||||
.set_file_name(entry.display_name())
|
||||
.save_file()
|
||||
{
|
||||
entry.save(&path).unwrap();
|
||||
ui.horizontal(|ui| {
|
||||
if ui.button("Save entry…").clicked() {
|
||||
if let Some(path) = rfd::FileDialog::new()
|
||||
.set_file_name(entry.display_name())
|
||||
.save_file()
|
||||
{
|
||||
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
21
utils/Cargo.toml
Normal 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
|
|
@ -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)]
|
|
@ -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")]
|
Loading…
Reference in a new issue