Compare commits

...

10 Commits

Author SHA1 Message Date
Lily Tsuru bd1d3dd8d3 feat(*): add testing CI configuration 2023-06-29 03:19:35 -04:00
Lily Tsuru ee7504f5f7 jmmt: move make_c_string to util 2023-06-29 03:12:06 -04:00
Lily Tsuru 7e97ccaf48 jmrenamer: Add "unclear" mode
This will allow transparent switching between clear names and .DAT names.
2023-06-29 00:10:18 -04:00
Lily Tsuru 94823a21e3 chore(*): cargo fmt 2023-06-28 19:27:15 -04:00
Lily Tsuru 15d438f294 jmrenamer: rewrite to use package.toc
The package.toc file contains all the file names, except for itself (but we know it always so this isn't a problem).

Therefore, we don't need to store a hardcoded string table! Yay!
2023-06-28 19:25:18 -04:00
Lily Tsuru 11abf7668a jmmt: Textool now works
Most of the POC reader code was moved into jmmt crate
2023-06-28 00:54:06 -04:00
Lily Tsuru 69e82236c6 jmmt: add texture formats
This commit also adds a skeleton for a texture tool.
2023-06-26 05:56:27 -04:00
Lily Tsuru 094c14a799 jmrenamer: print if a renamed filename already exists 2023-06-25 18:41:09 -04:00
Lily Tsuru d5f00adc95 jmrenamer: move DAT/MET filename code to jmmt crate 2023-06-25 18:39:44 -04:00
Lily Tsuru 88375f5581 imhex: import newer package structures 2023-06-25 18:19:01 -04:00
24 changed files with 715 additions and 115 deletions

View File

@ -5,3 +5,7 @@ end_of_line = lf
insert_final_newline = true insert_final_newline = true
indent_style = tab indent_style = tab
indent_size = 4 indent_size = 4
# spefcifically for YAML
[yml]
indent_style = space

16
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,16 @@
on:
push:
pull_request:
jobs:
build:
strategy:
matrix:
os: [ubuntu-latest, windows-latest]
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build
- name: Test
run: cargo test --verbose

View File

@ -2,6 +2,7 @@
#resolver = "2" #resolver = "2"
members = [ members = [
"crates/jmmt", "crates/jmmt",
"crates/jmrenamer" "crates/jmrenamer",
"crates/textool"
# "crates/paktool" # "crates/paktool"
] ]

View File

@ -5,3 +5,5 @@ edition = "2021"
[dependencies] [dependencies]
binext = "1.0.0" binext = "1.0.0"
image = "0.24.6"
thiserror = "1.0.40"

View File

@ -1,9 +1,13 @@
//! Low-level structure definitions //! Low-level file format definitions. These are suitable for usage with the [binext] crate,
//! which just so happens to be a dependency of this crate! Funny how things work.
pub mod package; pub mod package;
pub mod package_toc; pub mod package_toc;
pub mod ps2_palette;
pub mod ps2_texture;
/// A trait validatable format objects should implement. /// A trait validatable format objects should implement.
/// TODO: integrate this with some FourCC crate, or re-invent the wheel.
pub trait Validatable { pub trait Validatable {
/// Returns true if the object is valid, false otherwise. /// Returns true if the object is valid, false otherwise.
fn valid(&self) -> bool; fn valid(&self) -> bool;

View File

@ -1,7 +1,7 @@
//! Package file format structures //! Package file format structures
use crate::lzss::header::LzssHeader;
use super::Validatable; use super::Validatable;
use crate::lzss::header::LzssHeader;
/// "EOF" header. The QuickBMS script uses this to seek to the PGRP entry. /// "EOF" header. The QuickBMS script uses this to seek to the PGRP entry.
#[repr(C)] #[repr(C)]
@ -13,11 +13,12 @@ pub struct PackageEofHeader {
pub stringtable_size: u32, pub stringtable_size: u32,
/// Start offset of the [PackageGroup] in the package file. /// Start offset of the [PackageGroup] in the package file.
pub header_start_offset: u32 pub header_start_offset: u32,
} }
/// A Package Group. I have no idea what this is yet /// A Package Group. I have no idea what this is yet
#[repr(C)] #[repr(C)]
#[derive(Debug, Default)]
pub struct PackageGroup { pub struct PackageGroup {
pub fourcc: u32, pub fourcc: u32,
@ -28,7 +29,7 @@ pub struct PackageGroup {
pub group_file_count: u32, pub group_file_count: u32,
/// Padding. Set to a fill of 0xCD. /// Padding. Set to a fill of 0xCD.
pub pad: u32 pub pad: u32,
} }
impl PackageGroup { impl PackageGroup {
@ -44,6 +45,7 @@ impl Validatable for PackageGroup {
/// A package file chunk. /// A package file chunk.
#[repr(C)] #[repr(C)]
#[derive(Debug, Default)]
pub struct PackageFileChunk { pub struct PackageFileChunk {
pub fourcc: u32, pub fourcc: u32,
@ -85,10 +87,9 @@ pub struct PackageFileChunk {
pub file_uncompressed_size: u32, pub file_uncompressed_size: u32,
/// LZSS header. Only used if the file chunk is compressed. /// LZSS header. Only used if the file chunk is compressed.
pub lzss_header: LzssHeader pub lzss_header: LzssHeader,
} }
impl PackageFileChunk { impl PackageFileChunk {
/// 'PFIL' /// 'PFIL'
pub const VALID_FOURCC: u32 = 0x4C494650; pub const VALID_FOURCC: u32 = 0x4C494650;

View File

@ -1,5 +1,7 @@
//! Package.toc structures //! Package.toc structures
use crate::util::make_c_string;
/// An entry inside the `package.toc` file /// An entry inside the `package.toc` file
#[derive(Debug)] #[derive(Debug)]
#[repr(C)] #[repr(C)]
@ -14,23 +16,23 @@ pub struct PackageTocEntry {
} }
impl PackageTocEntry { impl PackageTocEntry {
fn file_name(&self) -> Option<String> { pub fn file_name(&self) -> Option<String> {
String::from_utf8(self.file_name.to_vec()).ok() make_c_string(&self.file_name)
} }
fn file_name_hash(&self) -> u32 { pub fn file_name_hash(&self) -> u32 {
self.file_name_hash self.file_name_hash
} }
fn toc_start_offset(&self) -> u32 { pub fn toc_start_offset(&self) -> u32 {
self.toc_start_offset self.toc_start_offset
} }
fn toc_size(&self) -> u32 { pub fn toc_size(&self) -> u32 {
self.toc_size self.toc_size
} }
fn toc_file_count(&self) -> u32 { pub fn toc_file_count(&self) -> u32 {
self.toc_file_count self.toc_file_count
} }
} }

View File

@ -0,0 +1,32 @@
//! .ps2_palette structures
use super::Validatable;
/// .ps2_palette header
#[repr(C)]
#[derive(Debug, Default)]
pub struct Ps2PaletteHeader {
pub fourcc: u32,
pub unk: u32,
pub unk2: u16,
pub color_count: u16,
pub palette_bpp: u16,
pub unk3: u16,
pub data_start: u32,
pub header_size: u32,
pub pad: [u32; 6], // reserved for game code, like .ps2_texture?
}
impl Ps2PaletteHeader {
/// 'PAL1'
pub const VALID_FOURCC: u32 = 0x314c4150;
}
impl Validatable for Ps2PaletteHeader {
fn valid(&self) -> bool {
self.fourcc == Self::VALID_FOURCC && self.header_size == 0x18
}
}

View File

@ -0,0 +1,38 @@
//! .ps2_texture structures
use super::Validatable;
/// .ps2_texture header.
#[repr(C)]
#[derive(Debug, Default)]
pub struct Ps2TextureHeader {
pub magic: u32,
pub unk: u32,
pub unk2: u16,
pub width: u16,
pub height: u16,
/// bits-per-pixel of the texture data. Anything above 8
/// will not have an associated .ps2_palette file,
/// since the texture data will not be indirect color.
pub bpp: u16,
/// Data start offset.
pub data_start_offset: u32,
/// Possibly the size of this header.
pub header_end_offset: u32,
pub unk6: [u32; 8], // mostly unrelated values, this is probably padding space for the game code to put stuff
}
impl Ps2TextureHeader {
/// 'TEX1'
pub const VALID_FOURCC: u32 = 0x31584554;
}
impl Validatable for Ps2TextureHeader {
fn valid(&self) -> bool {
self.magic == Self::VALID_FOURCC && self.header_end_offset == 0x38
}
}

View File

@ -65,9 +65,8 @@ impl Crc32Hash {
pub fn update_case_insensitive(&self, data: &[u8]) { pub fn update_case_insensitive(&self, data: &[u8]) {
for b in data.iter() { for b in data.iter() {
let old = self.state.take(); let old = self.state.take();
self.state.set( self.state
CRC32_TABLE[((old ^ (*b & !0x20) as u32) & 0xff) as usize] ^ (old >> 8), .set(CRC32_TABLE[((old ^ (*b & !0x20) as u32) & 0xff) as usize] ^ (old >> 8));
);
} }
} }

View File

@ -0,0 +1,24 @@
//! Utilities for making .DAT and .MET file names
//!
//! .DAT and .MET filenames are formatted like "{:X}.DAT" in fmt parlance.
//! The name component is the CRC32 of the original filename.
//!
//! The DAT/MET filename can be a max of 13 characters long.
use super::crc32::hash_string;
/// Make a .DAT filename from a cleartext filename.
pub fn dat_filename(filename: &str) -> String {
format!("{:X}.DAT", hash_string(String::from(filename)))
}
/// Make a .MET filename from a cleartext filename.
pub fn met_filename(filename: &str) -> String {
format!("{:X}.MET", hash_string(String::from(filename)))
}
/// Make a .DAT filename from a pre-created hash.
/// This is notably used to re-use hashes from `package.toc`.
pub fn dat_filename_from_hash(hash: u32) -> String {
format!("{:X}.DAT", hash)
}

View File

@ -1,4 +1,5 @@
//! Hash Algorithms //! Hash Algorithms
pub mod crc32; pub mod crc32;
pub mod filename;
pub use crc32::*; pub use crc32::*;

View File

@ -7,6 +7,11 @@ pub mod hash;
pub mod format; pub mod format;
pub mod lzss; pub mod lzss;
pub mod util;
// higher level I/O? // higher level I/O?
// pub mod read; pub mod read;
// pub mod write; // pub mod write;
// Maybe, using package.toc?
// pub mod pakfs;

View File

@ -1,7 +1,8 @@
use std::mem::size_of;
use crate::format::Validatable; use crate::format::Validatable;
use std::mem::size_of;
#[repr(C)] #[repr(C)]
#[derive(Debug, Default)]
pub struct LzssHeader { pub struct LzssHeader {
pub next: u32, // ps2 ptr. usually 0 cause theres no next header pub next: u32, // ps2 ptr. usually 0 cause theres no next header
pub byte_id: u8, pub byte_id: u8,

View File

@ -6,4 +6,3 @@
// - if we can't do that, investigate reimplementing compression based on `lzss` crate? // - if we can't do that, investigate reimplementing compression based on `lzss` crate?
pub mod header; pub mod header;

View File

@ -0,0 +1,5 @@
//! High-level readers
pub mod package_toc;
pub mod texture;

View File

@ -0,0 +1,45 @@
use std::fs::File;
use std::io::{Seek, SeekFrom};
use std::path::PathBuf;
use crate::format::package_toc::*;
use binext::BinaryRead;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("underlying I/O error: {0}")]
IoError(#[from] std::io::Error),
#[error("file too small to hold package toc entry")]
FileTooSmall,
/// Under-read of data
#[error("underread")]
ReadTooSmall,
}
pub type Result<T> = std::result::Result<T, Error>;
pub fn read_package_toc(path: PathBuf) -> Result<Vec<PackageTocEntry>> {
let mut toc_file = File::open(path.clone())?;
let file_size = toc_file.seek(SeekFrom::End(0))?;
toc_file.seek(SeekFrom::Start(0))?;
let vec_size: usize = file_size as usize / std::mem::size_of::<PackageTocEntry>();
if vec_size == 0 {
return Err(Error::FileTooSmall);
}
let mut vec: Vec<PackageTocEntry> = Vec::with_capacity(vec_size);
for _ in 0..vec_size {
vec.push(toc_file.read_binary::<PackageTocEntry>()?);
}
drop(toc_file);
Ok(vec)
}

View File

@ -0,0 +1,225 @@
//! High-level .ps2_texture reader
use std::fs::File;
use std::io::{Read, Seek, SeekFrom};
use std::path::{Path, PathBuf};
use binext::BinaryRead;
use thiserror::Error;
use crate::{
format::{ps2_palette::*, ps2_texture::*, Validatable},
util::Ps2Rgba,
};
#[derive(Error, Debug)]
pub enum Error {
#[error("underlying I/O error: {0}")]
IoError(#[from] std::io::Error),
/// Invalid texture/palette file header
#[error("invalid texture/palette file header")]
InvalidHeader,
/// Couldn't read enough bytes from either texture or palette file
#[error("read too few bytes in texture/palette file to complete texture")]
ReadTooSmall,
}
pub type Result<T> = std::result::Result<T, Error>;
/// High-level reader for PS2 textures.
pub struct Ps2TextureReader {
/// The initial path to the .ps2_texture file.
path: PathBuf,
texture_header: Ps2TextureHeader,
texture_data: Vec<u8>,
has_palette: bool,
palette_header: Ps2PaletteHeader,
palette_data: Vec<u8>,
}
impl Ps2TextureReader {
pub fn new(path: &Path) -> Self {
Ps2TextureReader {
path: path.to_path_buf(),
texture_header: Ps2TextureHeader::default(),
texture_data: Vec::default(),
has_palette: false,
palette_header: Ps2PaletteHeader::default(),
palette_data: Vec::default(),
}
}
/// Reads texture data into the reader. For most textures, this will mean we open/read
/// two files: the .ps2_texture file, and the .ps2_palette file.
///
/// This function does not parse or convert data, simply reads it for later conversion.
/// See [Ps2TextureReader::convert_to_image] for the function which does so.
pub fn read_data(&mut self) -> Result<()> {
// Open the .ps2_texture file
let mut texture_file = File::open(self.path.clone())?;
self.texture_header = texture_file.read_binary::<Ps2TextureHeader>()?;
if self.texture_header.valid() {
let texture_data_size: usize = ((self.texture_header.width as u32
* self.texture_header.height as u32)
* (self.texture_header.bpp / 8) as u32) as usize;
match self.texture_header.bpp {
8 => {
self.has_palette = true;
}
_ => {}
}
if self.has_palette {
let mut palette_path = self.path.clone();
// A lot of textures use the texture_<name>.b pattern.
// the palette for these is in a palette_<name>.b file, so we need to handle that specifically.
if let Some(ext) = self.path.extension() {
if ext == "b" {
match self.path.file_name() {
Some(name) => {
if let Some(name) = name.to_str() {
palette_path.set_file_name(
String::from(name).replace("texture_", "palette_"),
);
} else {
// this is a bad error to return here, but for now it works I guess
return Err(Error::InvalidHeader);
}
}
None => {
// ditto
return Err(Error::InvalidHeader);
}
}
} else {
// We can assume this came from a regular .ps2_texture file, and just set the extension.
palette_path.set_extension("ps2_palette");
}
}
let mut palette_file = File::open(palette_path)?;
self.palette_header = palette_file.read_binary::<Ps2PaletteHeader>()?;
if self.palette_header.valid() {
// There are no known files in JMMT which use a non-32bpp palette, so I consider this,
// while hacky, a "ok" assumption. if this isn't actually true then Oh Well
let pal_data_size = (self.palette_header.color_count * 4) as usize;
palette_file.seek(SeekFrom::Start(self.palette_header.data_start as u64))?;
// Read palette color data
self.palette_data.resize(pal_data_size, 0x0);
if palette_file.read(self.palette_data.as_mut_slice())? != pal_data_size {
return Err(Error::ReadTooSmall);
}
} else {
// I give up
return Err(Error::InvalidHeader);
}
}
texture_file.seek(SeekFrom::Start(
self.texture_header.data_start_offset as u64,
))?;
self.texture_data.resize(texture_data_size, 0x0);
if texture_file.read(self.texture_data.as_mut_slice())? != texture_data_size {
return Err(Error::ReadTooSmall);
}
} else {
return Err(Error::InvalidHeader);
}
Ok(())
}
fn palette_rgba_slice(&self) -> &[Ps2Rgba] {
assert_eq!(
self.palette_header.palette_bpp, 32,
"Palette BPP invalid for usage with palette_rgba_slice"
);
// Safety: The palette data buffer will always be 32-bits aligned, so the newly created slice
// will not end up reading any memory out of bounds of the Vec<u8> allocation.
// We assert this as true before returning.
return unsafe {
std::slice::from_raw_parts(
self.palette_data.as_ptr() as *const Ps2Rgba,
self.palette_header.color_count as usize,
)
};
}
/// Convert the read data into a image implemented by the [image] crate.
pub fn convert_to_image(&self) -> image::RgbaImage {
let mut image = image::RgbaImage::new(
self.texture_header.width as u32,
self.texture_header.height as u32,
);
if self.has_palette {
let palette_slice: &[Ps2Rgba] = self.palette_rgba_slice();
// this is shoddy and slow, but meh
for x in 0..self.texture_header.width as usize {
for y in 0..self.texture_header.height as usize {
image[(x as u32, y as u32)] = palette_slice
[self.texture_data[y * self.texture_header.width as usize + x] as usize]
.to_rgba();
}
}
} else {
match self.texture_header.bpp {
16 => {
let rgb565_slice = unsafe {
std::slice::from_raw_parts(
self.texture_data.as_ptr() as *const u16,
(self.texture_header.width as u32 * self.texture_header.height as u32)
as usize,
)
};
for x in 0..self.texture_header.width as usize {
for y in 0..self.texture_header.height as usize {
image[(x as u32, y as u32)] = Ps2Rgba::from_rgb565(
rgb565_slice[y * self.texture_header.width as usize + x],
)
.to_rgba();
}
}
}
// TODO: Find some less awful way to do this, that can be done without creating a new image object.
32 => {
let rgba_slice = unsafe {
std::slice::from_raw_parts(
self.texture_data.as_ptr() as *const Ps2Rgba,
(self.texture_header.width as u32 * self.texture_header.height as u32)
as usize,
)
};
for x in 0..self.texture_header.width as usize {
for y in 0..self.texture_header.height as usize {
image[(x as u32, y as u32)] =
rgba_slice[y * self.texture_header.width as usize + x].to_rgba();
}
}
}
_ => panic!(
"somehow got here with invalid bpp {}",
self.texture_header.bpp
),
}
}
image
}
}

View File

@ -0,0 +1,57 @@
//! General utility code, used throughout the JMMT crate
/// Like image::Rgba<u8>, but a safe C repressentation,
/// and alpha is multiplied to match PS2. Some helpers
/// are also provided to work with 16-bit colors.
#[derive(Clone)]
#[repr(C, packed)]
pub struct Ps2Rgba {
pub r: u8,
pub g: u8,
pub b: u8,
pub a: u8,
}
impl Ps2Rgba {
pub const fn to_rgba(&self) -> image::Rgba<u8> {
// avoid multiplication overflow
if self.a as u32 * 2 > 255 {
return image::Rgba::<u8>([self.r, self.g, self.b, 255]);
}
image::Rgba::<u8>([self.r, self.g, self.b, self.a * 2])
}
/// Create a instance from an rgb565 16bpp pixel.
pub const fn from_rgb565(value: u16) -> Ps2Rgba {
return Ps2Rgba {
r: ((value & 0x7C00) >> 7) as u8,
g: ((value & 0x03E0) >> 2) as u8,
b: ((value & 0x001F) << 3) as u8,
a: 255,
};
}
}
/// Make a Rust [String] from a byte slice that came from a C string/structure.
///
/// # Usage
///
/// The byte slice has to be a valid UTF-8 string.
/// (Note that in most cases, ASCII strings are valid UTF-8, so this isn't something you'll particularly
/// have to worry about).
///
/// # Safety
///
/// This function does not directly make use of any unsafe Rust code.
pub fn make_c_string(bytes: &[u8]) -> Option<String> {
let bytes_without_null = match bytes.iter().position(|&b| b == 0) {
Some(ix) => &bytes[..ix],
None => bytes,
};
match std::str::from_utf8(bytes_without_null).ok() {
Some(string) => Some(String::from(string)),
None => None,
}
}

View File

@ -4,4 +4,5 @@ version = "0.1.0"
edition = "2021" edition = "2021"
[dependencies] [dependencies]
clap = { version = "4.3.8", features = ["derive"] }
jmmt = { path = "../jmmt" } jmmt = { path = "../jmmt" }

View File

@ -1,74 +1,151 @@
//! A reimplemntation of jmmt_renamer in Rust. //! A reimplementation of jmmt_renamer in Rust.
//! This program should be run in the root directory //! This program should be run in the root directory
//! of an extracted (from image) copy of the game. //! of an extracted (from image) copy of the game.
use std::{fs, io, path::Path}; use jmmt::hash::filename::*;
use jmmt::hash::hash_string; use jmmt::read::package_toc::read_package_toc;
use std::{fs, path::Path};
const FILENAME_TABLE : [&str; 20] = [ use clap::{Subcommand, Parser};
// First loaded by the game
"package.toc",
// General packs
"config.pak",
// This file is referenced in the game files, #[derive(Parser)]
// but doesn't seem to exist anymore in the final build. #[command(author, version, about, long_about = None)]
//"shell.pak", #[command(propagate_version = true)]
struct Cli {
"shell_character_select.pak", #[command(subcommand)]
"shell_main.pak", command: Commands,
"shell_title.pak",
"shell_venue.pak",
"shell_event.pak",
"shell_option.pak",
"win_screens.pak",
// Game levels
"SF_san_fran.pak",
"DC_washington.pak",
"MK_MT_KILI.pak",
"MP_MACHU_PIHU.pak",
"LV_Las_Vegas.pak",
"AN_ANTARTICA.pak",
"NP_Nepal.pak",
"TH_TAHOE.pak",
"VA_Valdez_alaska.pak",
"RV_Rome.pak",
"TR_training.pak"
];
/// Make a .DAT filename from a cleartext filename.
///
/// .DAT and .MET filenames are formatted like "[hex char * 8].DAT"
/// The name component is the CRC32 of the original filename.
///
/// The DAT/MET filename can be a max of 13 characters long.
fn hashed_dat_filename(filename: &str) -> String {
format!("{:X}.DAT", hash_string(String::from(filename)))
} }
fn main() -> io::Result<()> { #[derive(Subcommand)]
enum Commands {
/// Rename the files in the DATA directory to the original filenames.
Clear,
// A relatively simple idiot-check. /// Rename the files to the .DAT filenames that the game expects.
if !Path::new("DATA").is_dir() { Unclear,
println!("This program should be run in the root of an extracted copy.");
std::process::exit(1);
} }
for clearname in FILENAME_TABLE.iter() { fn do_clear() {
let dat_filename = hashed_dat_filename(clearname); let package_toc_filename = format!("DATA/{}", dat_filename("package.toc"));
let dat_src = format!("DATA/{}", dat_filename);
let dat_clearname = format!("DATA/{}", String::from(*clearname));
match read_package_toc(Path::new(package_toc_filename.as_str()).to_path_buf()) {
Ok(toc) => {
for toc_entry in toc {
let dat_src = format!(
"DATA/{}",
dat_filename_from_hash(toc_entry.file_name_hash())
);
let src_path = Path::new(dat_src.as_str()); let src_path = Path::new(dat_src.as_str());
let dat_clearname = format!(
"DATA/{}",
toc_entry
.file_name()
.expect("How did invalid ASCII get here?")
);
let dest_path = Path::new(dat_clearname.as_str()); let dest_path = Path::new(dat_clearname.as_str());
if src_path.exists() { if src_path.exists() {
fs::rename(src_path, dest_path)?; match fs::rename(src_path, dest_path) {
println!("moved {} -> {}", src_path.display(), dest_path.display()); Ok(_) => {}
Err(error) => {
println!("Error renaming {}: {}", src_path.display(), error);
return ();
}
};
println!("Clearnamed {} -> {}", src_path.display(), dest_path.display());
} }
} }
Ok(()) match fs::rename(
Path::new(package_toc_filename.as_str()),
Path::new("DATA/package.toc"),
) {
Ok(_) => {}
Err(error) => {
println!("Error renaming TOC file: {}", error);
return ();
}
};
}
Err(error) => {
println!("Error reading package.toc file: {}", error);
return ();
}
}
}
fn do_unclear() {
let package_toc_filename = String::from("DATA/package.toc");
match read_package_toc(Path::new(package_toc_filename.as_str()).to_path_buf()) {
Ok(toc) => {
for toc_entry in toc {
let dat_clearname = format!(
"DATA/{}",
toc_entry
.file_name()
.expect("How did invalid ASCII get here?")
);
let src_path = Path::new(dat_clearname.as_str());
let dat_dest = format!(
"DATA/{}",
dat_filename_from_hash(toc_entry.file_name_hash())
);
let dest_path = Path::new(dat_dest.as_str());
if src_path.exists() {
match fs::rename(src_path, dest_path) {
Ok(_) => {}
Err(error) => {
println!("Error renaming {}: {}", src_path.display(), error);
return ();
}
};
println!("Uncleared {} -> {}", src_path.display(), dest_path.display());
}
}
let package_toc_dat_filename = format!("DATA/{}", dat_filename("package.toc"));
match fs::rename(
Path::new("DATA/package.toc"),
Path::new(package_toc_dat_filename.as_str()),
) {
Ok(_) => {}
Err(error) => {
println!("Error renaming TOC file: {}", error);
return ();
}
};
}
Err(error) => {
println!("Error reading package.toc file: {}", error);
return ();
}
}
}
fn main() {
// A relatively simple idiot-check. Later on utilities might have a shared library
// of code which validates game root stuff and can open it up/etc.
if !Path::new("DATA").is_dir() {
println!("This program should be run in the root of an extracted copy.");
return ();
}
let cli = Cli::parse();
match &cli.command {
Commands::Clear =>
do_clear(),
Commands::Unclear =>
do_unclear()
}
} }

View File

@ -0,0 +1,8 @@
[package]
name = "textool"
version = "0.1.0"
edition = "2021"
[dependencies]
clap = { version = "4.3.8", features = ["derive"] }
jmmt = { path = "../jmmt" }

View File

@ -0,0 +1,56 @@
use clap::{Parser, Subcommand};
use jmmt::read::texture::Ps2TextureReader;
use std::path::Path;
#[derive(Parser)]
#[command(author, version, about, long_about = None)]
#[command(propagate_version = true)]
struct Cli {
#[command(subcommand)]
command: Commands,
}
#[derive(Subcommand)]
enum Commands {
/// Exports the texture to a .png file
Export { path: String },
//Import
}
fn main() {
let cli = Cli::parse();
match &cli.command {
Commands::Export { path } => {
let path = Path::new(path);
if !path.is_file() {
println!("Need to provide a path to a file to export.");
return ();
}
let mut reader = Ps2TextureReader::new(path);
match reader.read_data() {
Ok(_) => {
let mut path = Path::new(path).to_path_buf();
path.set_extension("png");
match reader.convert_to_image().save(path.clone()) {
Ok(_) => {
println!("Wrote image {}", path.display())
}
Err(error) => {
println!("Error saving image: {}", error);
}
};
}
Err(error) => {
println!("Error reading texture data: {}", error);
return ();
}
}
}
}
}

View File

@ -30,12 +30,9 @@ struct Lzss_Header {
//} //}
}; };
// Data strutures actually part of the file: // "PGRP" chunk.
// "PGRP" entry.
// //
// This marks the start of a "package group". // This marks the start of a "package group".
// Whatever that is.
struct JMMT_PGRP { struct JMMT_PGRP {
u32 magic; u32 magic;
u32 groupNameCrc; // This is in the string table u32 groupNameCrc; // This is in the string table
@ -43,52 +40,55 @@ struct JMMT_PGRP {
u32 pad; // seemingly always 0xCDCDCDCD u32 pad; // seemingly always 0xCDCDCDCD
}; };
// "PFIL" entry. // "PFIL" chunk.
// //
// This represents a file block, // This represents a file chunk,
// which can itself represent either a whole file (> 65535 bytes), // which can itself represent either a whole file (when it is > 65536 bytes),
// or part of a file (which will need to be stiched together). // or part of a file (which will need to be stiched together from multiple chunks).
struct JMMT_PFIL { struct JMMT_PFIL {
u32 magic; u32 magic;
u32 unk[2]; // Don't know what these are? u32 unk; // These two seem to stay the same for every PFIL
u32 unk2;
// Sequence number of the chunk. // Sequence number of the chunk.
// This repressents the order of each chunk, // This repressents the order of each chunk,
// presumably so order can just be whatever. // presumably so order can just be whatever.
// //
// However the game seems to order chunks for files // However the packaging tool seems to leave files
// in order, and doesn't start/interleave other files // in order, and doesn't start/interleave other files
// in between. So this is a nice waste of 16 bits. // in between. This is definitely still useful, but
u16 chunkSequenceNumber; // not quite as much.
u16 chunkNumber;
// Amount of chunks which need to be read // Amount of chunks which need to be read
// from to read this file completely. // from to complete this file.
// //
// 1 means this file starts and ends on this chunk. // 1 means this file starts and ends on this chunk.
u16 chunkAmount; u16 chunkAmount;
// This is a CRC32 hash of the path of this file. // This is a CRC32 hash of the path of this file.
// //
// Hashed with jmmt::HashString() (in the jmmt_tools repo). // Hashed with jmmt::HashString().
u32 filenameCrc; u32 filenameCrc;
u32 unk2[7]; // more unknown stuff I don't care/know about u32 unk3[7]; // These stay the same per file chunk. Could be hashes
// Uncompressed size of this file chunk. Has a maximum of 65535 bytes. // Uncompressed size of this file chunk. Has a maximum of 65535 bytes.
u32 chunkSize; u32 chunkSize;
// Offset where this file chunk should start, // Offset where this file chunk should start,
// inside of a larger buffer. // inside of a larger buffer.
u32 blockOffset; u32 bufferOffset;
// ? // Compressed size of the chunk.
u32 unk3; u32 compressedSize;
// Offset inside of the package file where // Offset inside of the package file where
// the compressed data blob starts. // the compressed data blob starts.
u32 dataOffset; u32 dataOffset;
// Total file size.
u32 fileSize; u32 fileSize;
// TECH LZSS header. // TECH LZSS header.
@ -98,12 +98,10 @@ struct JMMT_PFIL {
// Debug information. This doesn't print literally everything, // Debug information. This doesn't print literally everything,
// just the useful stuff to look at it. // just the useful stuff to look at it.
if(1) { if(1) {
std::print(" Chunk seqNum: {}, Filename CRC: {:0x}, File Size: {}, Chunk Size: {}, Block Offset: {}, Data Offset: {}", chunkSequenceNumber, filenameCrc, fileSize, chunkSize, blockOffset, dataOffset); std::print("Hash: {:0x}, Seqnum: {}, ZOff: {}, FileSize: {}, ChunkSize: {}(z {}), BufferOff: {},",
filenameCrc, chunkNumber, dataOffset, fileSize,
chunkSize, compressedSize, bufferOffset );
} }
//if(lzHeader.cByteId == 0x91)
// std::print("file has a valid lzss header");
}; };
// This is a wrapper so we can easily do the chunk viewing in imhex. // This is a wrapper so we can easily do the chunk viewing in imhex.
@ -113,10 +111,10 @@ struct PFIL_WRAPPER {
JMMT_PFIL pfilChunkZero; JMMT_PFIL pfilChunkZero;
if(pfilChunkZero.chunkAmount != 1) { if(pfilChunkZero.chunkAmount != 1) {
std::print("This file has {} chunks", pfilChunkZero.chunkAmount); //std::print("This file has {} chunks", pfilChunkZero.chunkAmount);
JMMT_PFIL pfilChunkExtra[pfilChunkZero.chunkAmount - 1]; JMMT_PFIL pfilChunkExtra[pfilChunkZero.chunkAmount - 1];
} else { } else {
std::print("File ended with 1 chunk"); //std::print("File ended with 1 chunk");
} }
}; };
@ -126,6 +124,5 @@ struct PFIL_WRAPPER {
// Group header. // Group header.
JMMT_PGRP grp @ 0xd1c30; JMMT_PGRP grp @ 0xd1c30;
// This isn't right (as one PFIL chunk doesn't actually have to mean one file), // All pfil objects. The wrapper expands out other pfil chunks automatically. Pretty cool.
// but it works for testing and trying to understand the format.
PFIL_WRAPPER files[grp.nrfiles] @ $; PFIL_WRAPPER files[grp.nrfiles] @ $;